Feature/header (#177)
This commit is contained in:
parent
a5f3018211
commit
baba1c08eb
4
src/front/Assets/Icons/bell-notif.svg
Normal file
4
src/front/Assets/Icons/bell-notif.svg
Normal 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 |
@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
|
||||
import classes from "./classes.module.scss";
|
||||
import { IAppRule } from "@Front/Api/Entities/rule";
|
||||
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 HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink";
|
||||
import useToggle from "@Front/Hooks/useToggle";
|
||||
|
@ -1,9 +1,4 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
.burger-icon {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,37 @@
|
||||
import React from "react";
|
||||
import classes from "./classes.module.scss";
|
||||
import Image from "next/image";
|
||||
import BurgerIcon from "@Assets/Icons/burger.svg";
|
||||
import CrossIcon from "@Assets/Icons/cross.svg";
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
import { Bars3BottomRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import IconButton from "../../IconButton";
|
||||
import BurgerModal from "./BurgerModal";
|
||||
import classes from "./classes.module.scss";
|
||||
import WindowStore from "@Front/Stores/WindowStore";
|
||||
|
||||
type IProps = {
|
||||
isModalOpen: boolean;
|
||||
openBurgerMenu: () => void;
|
||||
closeBurgerMenu: () => void;
|
||||
};
|
||||
type IState = {
|
||||
// isModalOpen: boolean;
|
||||
const headerBreakpoint = 1023;
|
||||
|
||||
export default function BurgerMenu() {
|
||||
const { isOpen, toggle, close } = useOpenable();
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = (window: Window) => {
|
||||
if (window.innerWidth > headerBreakpoint && isOpen) close();
|
||||
};
|
||||
|
||||
export default class BurgerMenu extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
}
|
||||
const windowResizeSubscription = WindowStore.getInstance().onResize((window) => onResize(window));
|
||||
|
||||
return () => {
|
||||
windowResizeSubscription();
|
||||
};
|
||||
}, [close, isOpen]);
|
||||
|
||||
public override render(): JSX.Element {
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<Image
|
||||
alt="burger"
|
||||
src={this.props.isModalOpen ? CrossIcon : BurgerIcon}
|
||||
className={classes["burger-icon"]}
|
||||
onClick={this.props.openBurgerMenu}
|
||||
/>
|
||||
{this.props.isModalOpen && <BurgerModal isOpen={this.props.isModalOpen} closeModal={this.props.closeBurgerMenu} />}
|
||||
{isOpen ? (
|
||||
<IconButton icon={<XMarkIcon />} onClick={toggle} />
|
||||
) : (
|
||||
<IconButton icon={<Bars3BottomRightIcon />} onClick={toggle} />
|
||||
)}
|
||||
<BurgerModal isOpen={isOpen} closeModal={close} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import classNames from "classnames";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IHeaderLinkProps } from "../HeaderLink";
|
||||
import { IHeaderLinkProps } from "../ButtonHeader";
|
||||
|
||||
import Typography, { ETypo, ETypoColor } from "../../Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
|
@ -2,7 +2,5 @@
|
||||
|
||||
.root {
|
||||
display: inline-flex;
|
||||
@media screen and (max-width: $screen-ls) {
|
||||
display: none;
|
||||
}
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -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 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 HeaderLink from "../HeaderLink";
|
||||
import Menu, { IItem } from "../../Menu";
|
||||
import ButtonHeader from "../ButtonHeader";
|
||||
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() {
|
||||
const pathname = usePathname();
|
||||
|
||||
@ -73,7 +75,7 @@ export default function Navigation() {
|
||||
},
|
||||
]}>
|
||||
<>
|
||||
<HeaderLink
|
||||
<ButtonHeader
|
||||
text={"Dossiers en cours"}
|
||||
path={Module.getInstance().get().modules.pages.Folder.props.path}
|
||||
routesActive={[
|
||||
@ -81,19 +83,47 @@ export default function Navigation() {
|
||||
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
|
||||
]}
|
||||
/>
|
||||
<HeaderLink
|
||||
<ButtonHeader
|
||||
text={"Dossiers archivés"}
|
||||
path={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>
|
||||
<HeaderSubmenu
|
||||
text={"Espace office"}
|
||||
links={[
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const officeItems: IItem[] = [
|
||||
{
|
||||
icon: <UsersIcon />,
|
||||
text: "Collaborateurs",
|
||||
path: Module.getInstance().get().modules.pages.Collaborators.props.path,
|
||||
link: Module.getInstance().get().modules.pages.Collaborators.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.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",
|
||||
path: Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
link: Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Roles.pages.Create.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",
|
||||
path: Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
link: Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.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",
|
||||
path: Module.getInstance().get().modules.pages.OfficesRib.props.path,
|
||||
link: Module.getInstance().get().modules.pages.OfficesRib.props.path,
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
@ -150,8 +183,9 @@ export default function Navigation() {
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <TagIcon />,
|
||||
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: [
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
|
||||
@ -168,23 +202,13 @@ export default function Navigation() {
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* </Rules> */}
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.officeRoles,
|
||||
},
|
||||
]}>
|
||||
<HeaderSubmenu
|
||||
text={"Espace super admin"}
|
||||
links={[
|
||||
];
|
||||
|
||||
const superAdminItems: IItem[] = [
|
||||
{
|
||||
icon: <UsersIcon />,
|
||||
text: "Gestion des utilisateurs",
|
||||
path: Module.getInstance().get().modules.pages.Users.props.path,
|
||||
link: Module.getInstance().get().modules.pages.Users.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Users.props.path,
|
||||
@ -197,8 +221,9 @@ export default function Navigation() {
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <UsersIcon />,
|
||||
text: "Gestion des offices",
|
||||
path: Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
link: Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
@ -210,9 +235,4 @@ export default function Navigation() {
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
];
|
||||
|
@ -1,18 +1,4 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
.icon-container {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.notification-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
.notification-dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +1,54 @@
|
||||
import InfoIcon from "@Assets/Icons/info.svg";
|
||||
import NotificationIcon from "@Assets/Icons/notification.svg";
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
import Toasts, { IToast } from "@Front/Stores/Toasts";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { BellIcon } from "@heroicons/react/24/outline";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import IconButton from "../../IconButton";
|
||||
import classes from "./classes.module.scss";
|
||||
import NotificationModal from "./NotificationModal";
|
||||
|
||||
type IProps = {
|
||||
isModalOpen: boolean;
|
||||
openNotificationModal: () => void;
|
||||
closeNotificationModal: () => void;
|
||||
};
|
||||
type IState = {
|
||||
hasNotifications: boolean;
|
||||
toastList: IToast[] | null;
|
||||
export default function Notifications() {
|
||||
const [_toastList, setToastList] = useState<IToast[] | null>(Toasts.getInstance().toasts);
|
||||
const [hasNotifications, setHasNotifications] = useState<boolean>(Toasts.getInstance().toasts.length > 0);
|
||||
const { isOpen, close, toggle } = useOpenable();
|
||||
|
||||
useEffect(() => {
|
||||
const handleToastChange = (newToastList: IToast[] | null) => {
|
||||
setToastList(newToastList);
|
||||
setHasNotifications(newToastList ? newToastList.length > 0 : false);
|
||||
};
|
||||
|
||||
export default class Notifications extends React.Component<IProps, IState> {
|
||||
private removeOnToastChange: () => void = () => {};
|
||||
const removeOnToastChange = Toasts.getInstance().onChange(handleToastChange);
|
||||
|
||||
constructor(props: IProps) {
|
||||
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
|
||||
return () => {
|
||||
removeOnToastChange();
|
||||
};
|
||||
this.handleToastChange = this.handleToastChange.bind(this);
|
||||
}
|
||||
}, []);
|
||||
|
||||
public override render(): JSX.Element {
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["icon-container"]} onClick={this.props.openNotificationModal}>
|
||||
<Image alt="notifications" src={NotificationIcon} className={classes["notification-icon"]} />
|
||||
{this.state.hasNotifications && (
|
||||
<Image className={classes["notification-dot"]} src={InfoIcon} alt="Unread notification" />
|
||||
{hasNotifications ? (
|
||||
<IconButton icon={<BellNotifIcon />} onClick={toggle} />
|
||||
) : (
|
||||
<IconButton icon={<BellIcon />} onClick={toggle} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{this.props.isModalOpen && (
|
||||
<NotificationModal isOpen={this.props.isModalOpen} closeModal={this.props.closeNotificationModal} />
|
||||
)}
|
||||
<NotificationModal isOpen={isOpen} closeModal={close} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public override componentDidMount() {
|
||||
this.removeOnToastChange = Toasts.getInstance().onChange(this.handleToastChange);
|
||||
}
|
||||
|
||||
public override componentWillUnmount() {
|
||||
this.removeOnToastChange();
|
||||
}
|
||||
|
||||
private handleToastChange(toastList: IToast[] | null) {
|
||||
this.setState({
|
||||
toastList,
|
||||
hasNotifications: toastList ? toastList.length > 0 : false,
|
||||
});
|
||||
}
|
||||
function BellNotifIcon() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
.profile-icon {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 Image from "next/image";
|
||||
import ProfileIcon from "@Assets/Icons/user.svg";
|
||||
import ProfileModal from "./ProfileModal";
|
||||
|
||||
type IProps = {
|
||||
isModalOpen: boolean;
|
||||
openProfileModal: () => void;
|
||||
closeProfileModal: () => void;
|
||||
};
|
||||
type IState = {};
|
||||
const headerBreakpoint = 1023;
|
||||
|
||||
export default class Profile extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isModalOpen: false,
|
||||
};
|
||||
}
|
||||
export default function Profile() {
|
||||
const { isOpen, toggle, close } = useOpenable();
|
||||
|
||||
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 (
|
||||
<div className={classes["root"]}>
|
||||
<Image alt="profile" src={ProfileIcon} className={classes["profile-icon"]} onClick={this.props.openProfileModal} />
|
||||
{this.props.isModalOpen && <ProfileModal isOpen={this.props.isModalOpen} closeModal={this.props.closeProfileModal} />}
|
||||
<IconButton icon={<UserIcon />} onClick={toggle} />
|
||||
<ProfileModal isOpen={isOpen} closeModal={close} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,20 @@
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 83px;
|
||||
background-color: var(--color-generic-white);
|
||||
box-shadow: $shadow-nav;
|
||||
padding: 0 48px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 75px;
|
||||
padding: 0px var(--spacing-lg, 24px);
|
||||
|
||||
border-bottom: 1px solid var(--menu-border, #d7dce0);
|
||||
background: var(--menu-background, #fff);
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
|
||||
@media (max-width: $screen-m) {
|
||||
transition: transform 500ms ease-out;
|
||||
|
||||
&[data-open="closed"] {
|
||||
transform: translateY(-85px);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
> a {
|
||||
cursor: pointer !important;
|
||||
@ -31,45 +27,22 @@
|
||||
}
|
||||
|
||||
.right-section {
|
||||
.profile-section {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
@media (max-width: $screen-ls) {
|
||||
.desktop {
|
||||
@media (max-width: $screen-m) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-section {
|
||||
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 {
|
||||
.mobile {
|
||||
display: none;
|
||||
|
||||
@media (max-width: $screen-ls) {
|
||||
@media (max-width: $screen-m) {
|
||||
display: inline-flex;
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 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 { 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 Notifications from "./Notifications";
|
||||
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 = {
|
||||
isUserConnected: boolean;
|
||||
};
|
||||
|
||||
type IPropsClass = IProps & {
|
||||
isOnCustomerLoginPage: boolean;
|
||||
};
|
||||
export default function Header(props: IProps) {
|
||||
const { isUserConnected } = props;
|
||||
|
||||
type IState = {
|
||||
open: EHeaderOpeningState;
|
||||
isBurgerMenuOpen: boolean;
|
||||
isNotificationMenuOpen: boolean;
|
||||
isProfileMenuOpen: boolean;
|
||||
cancelAt: Date | null;
|
||||
};
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const isOnCustomerLoginPage = Module.getInstance().get().modules.pages.CustomersLogin.props.path === pathname;
|
||||
|
||||
class HeaderClass extends React.Component<IPropsClass, IState> {
|
||||
private onWindowResize = () => {};
|
||||
private headerBreakpoint = 1300;
|
||||
const [cancelAt, setCancelAt] = useState<Date | null>(null);
|
||||
|
||||
constructor(props: IPropsClass) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
open: EHeaderOpeningState.OPEN,
|
||||
isBurgerMenuOpen: false,
|
||||
isNotificationMenuOpen: false,
|
||||
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);
|
||||
const loadSubscription = useCallback(async () => {
|
||||
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) {
|
||||
setCancelAt(new Date(stripeSubscription.cancel_at! * 1000));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
|
||||
public override render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className={classes["root"]} data-open={this.state.open}>
|
||||
<div className={classes["root"]}>
|
||||
<Head>
|
||||
<link rel="shortcut icon" href={"/favicon.svg"} />
|
||||
</Head>
|
||||
<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"]} />
|
||||
</Link>
|
||||
</div>
|
||||
{this.props.isUserConnected && (
|
||||
{isUserConnected && (
|
||||
<>
|
||||
<div className={classes["desktop"]}>
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className={classes["right-section"]}>
|
||||
<div className={classes["help-section"]}>
|
||||
<Link href="https://tally.so/r/mBGaNY" target="blank">
|
||||
<Image src={LifeBuoy} alt="help" />
|
||||
<div className={classes["desktop"]}>
|
||||
<Link href="https://tally.so/r/mBGaNY" target="_blank">
|
||||
<IconButton icon={<LifebuoyIcon />} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes["notification-section"]}>
|
||||
<Notifications
|
||||
isModalOpen={this.state.isNotificationMenuOpen}
|
||||
openNotificationModal={this.openNotificationMenu}
|
||||
closeNotificationModal={this.closeNotificationMenu}
|
||||
/>
|
||||
<Notifications />
|
||||
<div className={classes["desktop"]}>
|
||||
<Profile />
|
||||
</div>
|
||||
<div className={classes["profile-section"]}>
|
||||
<Profile
|
||||
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 className={classes["mobile"]}>
|
||||
<BurgerMenu />
|
||||
</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>
|
||||
{this.state.cancelAt && (
|
||||
{cancelAt && (
|
||||
<div className={classes["subscription-line"]}>
|
||||
<InformationCircleIcon height="24" />;
|
||||
<InformationCircleIcon height="24" />
|
||||
<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{" "}
|
||||
{this.state.cancelAt.toLocaleDateString()}.
|
||||
{cancelAt.toLocaleDateString()}.
|
||||
</Typography>
|
||||
</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} />;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
.menu-item-wrapper {
|
||||
.root {
|
||||
width: 100%;
|
||||
.menu-item {
|
||||
display: flex;
|
65
src/front/Components/DesignSystem/Menu/MenuItem/index.tsx
Normal file
65
src/front/Components/DesignSystem/Menu/MenuItem/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -5,25 +5,35 @@
|
||||
.sub-menu {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
|
||||
display: inline-flex;
|
||||
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
|
||||
flex-direction: column;
|
||||
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);
|
||||
background: var(--color-generic-white, #fff);
|
||||
|
||||
text-wrap: nowrap;
|
||||
/* shadow/sm */
|
||||
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1);
|
||||
z-index: 2;
|
||||
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 3;
|
||||
|
||||
&[data-opening-side="left"] {
|
||||
left: auto;
|
||||
right: 0px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&[data-opening-side="right"] {
|
||||
left: 0px;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
&[data-opening-side="center"] {
|
||||
transform: translateX(-25%);
|
||||
top: 90px;
|
||||
}
|
||||
}
|
||||
}
|
91
src/front/Components/DesignSystem/Menu/index.tsx
Normal file
91
src/front/Components/DesignSystem/Menu/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -153,6 +153,7 @@ export enum ETypoColor {
|
||||
CONTRAST_DEFAULT = "--contrast-default",
|
||||
CONTRAST_HOVERED = "--contrast-hovered",
|
||||
ERROR_WEAK_CONTRAST = "--error-weak-contrast",
|
||||
CONTRAST_ACTIVED = "--contrast-actived",
|
||||
NAVIGATION_BUTTON_CONTRAST_DEFAULT = "--navigation-button-contrast-default",
|
||||
NAVIGATION_BUTTON_CONTRAST_ACTIVE = "--navigation-button-contrast-active",
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -26,7 +26,7 @@ export default function Rules(props: IProps) {
|
||||
if (props.mode === RulesMode.NECESSARY) {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import classes from "./classes.module.scss";
|
||||
import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu";
|
||||
import Menu from "@Front/Components/DesignSystem/Menu";
|
||||
|
||||
export default function DesignSystem() {
|
||||
const { isOpen, open, close } = useOpenable();
|
||||
@ -80,9 +80,8 @@ export default function DesignSystem() {
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["components"]}>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Button icon with menu</Typography>
|
||||
<ButtonWithSubMenu
|
||||
icon={<EllipsisHorizontalIcon />}
|
||||
subElements={[
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
icon: <UsersIcon />,
|
||||
text: "Modifier les collaborateurs",
|
||||
@ -101,8 +100,9 @@ export default function DesignSystem() {
|
||||
link: "/",
|
||||
color: ETypoColor.ERROR_WEAK_CONTRAST,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
]}>
|
||||
<IconButton icon={<EllipsisHorizontalIcon />} variant={EIconButtonVariant.NEUTRAL} />
|
||||
</Menu>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography>
|
||||
<Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography>
|
||||
<NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} />
|
||||
|
@ -1,17 +1,18 @@
|
||||
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 Module from "@Front/Config/Module";
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
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 { AnchorStatus } from "../..";
|
||||
import classes from "./classes.module.scss";
|
||||
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 = {
|
||||
customer: ICustomer;
|
||||
@ -56,10 +57,9 @@ export default function ClientBox(props: IProps) {
|
||||
{customer.contact?.first_name} {customer.contact?.last_name}
|
||||
</Typography>
|
||||
{anchorStatus === AnchorStatus.NOT_ANCHORED && (
|
||||
<ButtonWithSubMenu
|
||||
icon={<PencilSquareIcon />}
|
||||
<Menu
|
||||
openingSide="right"
|
||||
subElements={[
|
||||
items={[
|
||||
{
|
||||
icon: <UsersIcon />,
|
||||
text: "Modifier les informations",
|
||||
@ -74,12 +74,9 @@ export default function ClientBox(props: IProps) {
|
||||
text: "Modifier la note",
|
||||
link: createOrUpdateNotePath,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
// <Link
|
||||
// href={}>
|
||||
// <IconButton variant={EIconButtonVariant.NEUTRAL} />
|
||||
// </Link>
|
||||
]}>
|
||||
<IconButton icon={<PencilSquareIcon />} variant={EIconButtonVariant.NEUTRAL} />
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
|
@ -1,15 +1,16 @@
|
||||
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 Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import Module from "@Front/Config/Module";
|
||||
import { ArchiveBoxIcon, EllipsisHorizontalIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline";
|
||||
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 { AnchorStatus } from "..";
|
||||
import classes from "./classes.module.scss";
|
||||
|
||||
type IProps = {
|
||||
folder: OfficeFolder | null;
|
||||
progress: number;
|
||||
@ -22,7 +23,7 @@ export default function InformationSection(props: IProps) {
|
||||
const { folder, progress, onArchive, anchorStatus, isArchived } = props;
|
||||
|
||||
const getSubMenuElement = useCallback(() => {
|
||||
let elements: ISubElement[] = [];
|
||||
let elements: IItem[] = [];
|
||||
|
||||
// Creating the three elements and adding them conditionnally
|
||||
const modifyCollaboratorsElement = {
|
||||
@ -86,7 +87,9 @@ export default function InformationSection(props: IProps) {
|
||||
<div className={classes["progress-container"]}>
|
||||
<CircleProgress percentage={progress} />
|
||||
<div className={classes["icon-container"]}>
|
||||
<ButtonWithSubMenu icon={<EllipsisHorizontalIcon />} subElements={getSubMenuElement()} />
|
||||
<Menu items={getSubMenuElement()}>
|
||||
<IconButton icon={<EllipsisHorizontalIcon />} variant={EIconButtonVariant.NEUTRAL} />
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["description-container"]}>
|
||||
|
Loading…
x
Reference in New Issue
Block a user