burger modal

This commit is contained in:
Max S 2024-07-25 12:30:02 +02:00
parent 5efd8b3713
commit f59f7ad9db
17 changed files with 355 additions and 531 deletions

View File

@ -1,18 +0,0 @@
@import "@Themes/constants.scss";
.root {
.content {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
}
.sub-menu {
padding: 24px;
text-align: center;
gap: 24px;
display: flex;
flex-direction: column;
}
}

View File

@ -1,58 +0,0 @@
import classNames from "classnames";
import { useRouter } from "next/router";
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 "../../../ButtonHeader";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink";
import useToggle from "@Front/Hooks/useToggle";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
type IProps = {
text: string | JSX.Element;
links: (IHeaderLinkProps & {
rules?: IAppRule[];
})[];
};
export default function HeaderSubmenu(props: IProps) {
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = useState(true);
const { active: isSubmenuOpened, toggle } = useToggle();
useEffect(() => {
setIsActive(false);
if (props.links.some((link) => link.path === pathname)) setIsActive(true);
if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true);
}, [isActive, pathname, props.links]);
return (
<Rules mode={RulesMode.OPTIONAL} rules={props.links.flatMap((link) => link.rules ?? [])}>
<div className={classes["container"]}>
<div className={classNames(classes["root"], (isActive || isSubmenuOpened) && classes["active"])}>
<div className={classes["content"]} onClick={toggle}>
<Typography
typo={isActive || isSubmenuOpened ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isSubmenuOpened ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{props.text}
</Typography>
{isSubmenuOpened ? <ChevronUpIcon height="20" width="20" /> : <ChevronDownIcon height="20" width="20" />}
</div>
<div className={classes["underline"]} data-active={(isActive || isSubmenuOpened).toString()} />
{isSubmenuOpened && (
<div className={classes["sub-menu"]}>
{props.links.map((link) => (
<Rules mode={RulesMode.NECESSARY} rules={link.rules ?? []} key={link.path}>
<HeaderSubmenuLink {...link} />
</Rules>
))}
</div>
)}
</div>
</div>
</Rules>
);
}

View File

@ -1,21 +1,22 @@
@import "@Themes/constants.scss"; @import "@Themes/constants.scss";
.root { .root {
position: absolute;
top: var(--header-height);
left: 0;
width: 100%;
max-height: calc(100vh - var(--header-height));
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--color-generic-white);
box-shadow: $shadow-nav;
padding: 24px;
position: absolute;
top: 83px;
width: 100%;
left: 0;
text-align: center;
max-height: calc(100vh - var(--header-height));
overflow: auto; overflow: auto;
> *:not(:last-child) {
margin-bottom: 24px; border-radius: var(--menu-radius, 0px);
} background: var(--color-generic-white, #FFF);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.10);
.separator { .separator {
width: 100%; width: 100%;

View File

@ -1,170 +1,196 @@
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import LogOutButton from "@Front/Components/DesignSystem/LogOutButton"; import LogOutButton from "@Front/Components/DesignSystem/LogOutButton";
import MenuItem from "@Front/Components/DesignSystem/Menu/MenuItem";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import Module from "@Front/Config/Module"; import Module from "@Front/Config/Module";
import React from "react"; import React from "react";
import NavigationLink from "../../NavigationLink";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import BurgerModalSubmenu from "./BurgerModalSubmenu";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
type IProps = { type IProps = {
isOpen: boolean; isOpen: boolean;
closeModal: () => void; closeModal: () => void;
}; };
type IState = {};
export default class BurgerModal extends React.Component<IProps, IState> { export default function BurgerModal(props: IProps) {
// TODO isEnabled depending on role given by DB const { isOpen, closeModal } = props;
public override render(): JSX.Element | null { if (!isOpen) return null;
if (!this.props.isOpen) return null;
return ( return (
<> <>
<div className={classes["background"]} onClick={this.props.closeModal} /> <div className={classes["background"]} onClick={closeModal} />
<div className={classes["root"]}> <div className={classes["root"]}>
<Rules <Rules
mode={RulesMode.OPTIONAL} mode={RulesMode.OPTIONAL}
rules={[ rules={[
{ {
action: AppRuleActions.read, action: AppRuleActions.read,
name: AppRuleNames.officeFolders, name: AppRuleNames.officeFolders,
}, },
]}> ]}>
<> <>
<NavigationLink <MenuItem
path={Module.getInstance().get().modules.pages.Folder.props.path} item={{
text="Dossiers en cours" text: "Dossiers en cours",
routesActive={[ routesActive: [
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path, Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path, Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]} ],
/> link: Module.getInstance().get().modules.pages.Folder.props.path,
<NavigationLink }}
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path} />
text="Dossiers archivés"
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
<div className={classes["separator"]} />
</>
</Rules>
<BurgerModalSubmenu <MenuItem
text={"Espace super admin"} item={{
links={[ text: "Dossiers archivés",
{ routesActive: [Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path],
text: "Gestion des utilisateurs", link: Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path,
path: Module.getInstance().get().modules.pages.Users.props.path, hasSeparator: true,
routesActive: [ }}
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path, />
Module.getInstance().get().modules.pages.Users.props.path, </>
], </Rules>
rules: [
{ <MenuItem
action: AppRuleActions.update, item={{
name: AppRuleNames.offices, text: "Espace super admin",
}, dropdown: {
], items: [
}, {
{ text: "Gestion des utilisateurs",
text: "Gestion des offices", link: Module.getInstance().get().modules.pages.Users.props.path,
path: Module.getInstance().get().modules.pages.Offices.props.path, routesActive: [
routesActive: [ Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path, Module.getInstance().get().modules.pages.Users.props.path,
Module.getInstance().get().modules.pages.Offices.props.path, ],
], rules: [
rules: [ {
{ action: AppRuleActions.update,
action: AppRuleActions.update, name: AppRuleNames.offices,
name: AppRuleNames.offices, },
}, ],
], },
}, ],
]} },
/> }}
<BurgerModalSubmenu />
text="Espace office"
links={[ <MenuItem
{ item={{
text: "Collaborateurs", text: "Espace office",
path: Module.getInstance().get().modules.pages.Collaborators.props.path, hasSeparator: true,
routesActive: [ dropdown: {
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path, items: [
Module.getInstance().get().modules.pages.Collaborators.props.path, {
], text: "Collaborateurs",
rules: [ link: Module.getInstance().get().modules.pages.Collaborators.props.path,
{ routesActive: [
action: AppRuleActions.update, Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path,
name: AppRuleNames.users, Module.getInstance().get().modules.pages.Collaborators.props.path,
}, ],
], rules: [
}, {
{ action: AppRuleActions.update,
text: "Gestion des rôles", name: AppRuleNames.users,
path: 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, {
Module.getInstance().get().modules.pages.Roles.props.path, text: "Gestion des rôles",
], link: Module.getInstance().get().modules.pages.Roles.props.path,
rules: [ routesActive: [
{ Module.getInstance().get().modules.pages.Roles.pages.Create.props.path,
action: AppRuleActions.update, Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
name: AppRuleNames.officeRoles, Module.getInstance().get().modules.pages.Roles.props.path,
}, ],
], rules: [
}, {
{ action: AppRuleActions.update,
text: "Paramétrage des listes de pièces", name: AppRuleNames.officeRoles,
path: 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, {
Module.getInstance().get().modules.pages.DeedTypes.props.path, text: "Paramétrage des listes de pièces",
Module.getInstance().get().modules.pages.DocumentTypes.props.path, link: Module.getInstance().get().modules.pages.DeedTypes.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path, routesActive: [
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path, Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path, Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
], Module.getInstance().get().modules.pages.DeedTypes.props.path,
rules: [ Module.getInstance().get().modules.pages.DocumentTypes.props.path,
{ Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
action: AppRuleActions.update, Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
name: AppRuleNames.deedTypes, Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
}, ],
], rules: [
}, {
{ action: AppRuleActions.update,
text: "RIB Office", name: AppRuleNames.deedTypes,
path: Module.getInstance().get().modules.pages.OfficesRib.props.path, },
rules: [ ],
{ },
action: AppRuleActions.update, {
name: AppRuleNames.rib, text: "RIB Office",
}, link: Module.getInstance().get().modules.pages.OfficesRib.props.path,
], rules: [
}, {
{ action: AppRuleActions.update,
text: "Abonnement", name: AppRuleNames.rib,
path: 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, {
Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path, text: "Abonnement",
Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path, link: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path, routesActive: [
Module.getInstance().get().modules.pages.Subscription.pages.New.props.path, Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Subscribe.props.path, Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
], Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path,
}, Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
]} Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path,
/> Module.getInstance().get().modules.pages.Subscription.pages.New.props.path,
<div className={classes["separator"]} /> Module.getInstance().get().modules.pages.Subscription.pages.Subscribe.props.path,
<NavigationLink path={Module.getInstance().get().modules.pages.MyAccount.props.path} text="Mon compte" /> ],
<NavigationLink target="blank" path="https://ressources.lecoffre.io/" text="Guide de Prise en Main" /> },
<NavigationLink target="blank" path="https://tally.so/r/mBGaNY" text="Support" /> ],
<NavigationLink target="blank" path="/CGU_LeCoffre_io.pdf" text="CGU" /> },
<LogOutButton /> }}
</div> />
</>
); <MenuItem
} item={{
text: "Mon compte",
link: Module.getInstance().get().modules.pages.MyAccount.props.path,
hasSeparator: true,
}}
/>
<MenuItem
item={{
text: "Guide de Prise en Main",
link: "https://ressources.lecoffre.io/",
target: "_blank",
}}
/>
<MenuItem
item={{
text: "Support",
link: "https://tally.so/r/mBGaNY",
target: "_blank",
}}
/>
<MenuItem
item={{
text: "CGU",
link: "/CGU_LeCoffre_io.pdf",
hasSeparator: true,
}}
/>
<LogOutButton />
</div>
</>
);
} }

View File

@ -1,39 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import useHoverable from "@Front/Hooks/useHoverable";
type IHeaderLinkProps = {
text: string | JSX.Element;
path: string;
routesActive?: string[];
};
export default function HeaderSubmenuLink(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]);
return (
<Link href={props.path} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<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>
</Link>
);
}

View File

@ -1,44 +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;
}
.sub-menu {
box-shadow: 0px 8px 10px 0px #00000012;
padding: 24px;
text-align: center;
gap: 24px;
left: 0;
transform: translateX(-25%);
width: 300px;
top: 84px;
display: flex;
flex-direction: column;
background: white;
position: absolute;
}
}

View File

@ -1,57 +0,0 @@
import classNames from "classnames";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { IHeaderLinkProps } from "../ButtonHeader";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import useHoverable from "@Front/Hooks/useHoverable";
import HeaderSubmenuLink from "./HeaderSubmenuLink";
import { IAppRule } from "@Front/Api/Entities/rule";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
type IProps = {
text: string | JSX.Element;
links: (IHeaderLinkProps & {
rules?: IAppRule[];
})[];
};
export default function HeaderSubmenu(props: IProps) {
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = useState(false);
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable(100);
useEffect(() => {
setIsActive(false);
if (props.links.some((link) => link.path === pathname)) setIsActive(true);
if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true);
}, [isActive, pathname, props.links]);
return (
<Rules mode={RulesMode.OPTIONAL} rules={props.links.flatMap((link) => link.rules ?? [])}>
<div className={classes["container"]} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div className={classNames(classes["root"], (isActive || isHovered) && classes["active"])}>
<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()} />
{isHovered && (
<div className={classes["sub-menu"]}>
{props.links.map((link) => (
<Rules mode={RulesMode.NECESSARY} rules={link.rules ?? []} key={link.path}>
<HeaderSubmenuLink {...link} />
</Rules>
))}
</div>
)}
</div>
</div>
</Rules>
);
}

View File

@ -8,7 +8,8 @@ import { AdjustmentsVerticalIcon, BanknotesIcon, Square3Stack3DIcon, TagIcon, Us
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import Menu, { IItem } from "../../Menu"; import Menu from "../../Menu";
import { IItem } from "../../Menu/MenuItem";
import ButtonHeader from "../ButtonHeader"; import ButtonHeader from "../ButtonHeader";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";

View File

@ -1,12 +0,0 @@
@import "@Themes/constants.scss";
.root {
display: flex;
position: relative;
width: fit-content;
margin: auto;
.content {
align-content: center;
}
}

View File

@ -1,54 +0,0 @@
import React from "react";
import classes from "./classes.module.scss";
import Link from "next/link";
import classNames from "classnames";
import { useRouter } from "next/router";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
type IProps = {
text: string | JSX.Element;
path?: string;
onClick?: () => void;
isEnabled?: boolean;
isActive?: boolean;
routesActive?: string[];
target?: "blank" | "self" | "_blank";
};
type IPropsClass = IProps;
type IStateClass = {};
class NavigationLinkClass extends React.Component<IPropsClass, IStateClass> {
static defaultProps = { isEnabled: true };
public override render(): JSX.Element | null {
if (!this.props.isEnabled) return null;
return (
<Link
href={this.props.path ?? ""}
className={classNames(classes["root"], this.props.isActive && [classes["active"]])}
onClick={this.props.onClick}
target={this.props.target}>
<div className={classes["content"]}>
<Typography
typo={this.props.isActive ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={this.props.isActive ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{this.props.text}
</Typography>
</div>
</Link>
);
}
}
export default function NavigationLink(props: IProps) {
const router = useRouter();
const { pathname } = router;
let isActive = props.path === pathname;
if (props.routesActive) {
for (const routeActive of props.routesActive) {
if (isActive) break;
isActive = pathname.includes(routeActive);
}
}
return <NavigationLinkClass {...props} isActive={isActive} />;
}

View File

@ -1,14 +1,26 @@
@import "@Themes/constants.scss"; @import "@Themes/constants.scss";
.root { .root {
display: flex;
flex-direction: column;
background-color: var(--color-generic-white);
box-shadow: $shadow-nav;
padding: 24px;
position: absolute; position: absolute;
top: 107px; top: 48px;
right: 66px;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
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;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
z-index: 3;
top: calc(var(--header-height) + 10px);
right: 32px;
text-align: center; text-align: center;
animation: smooth-appear 0.2s ease forwards; animation: smooth-appear 0.2s ease forwards;
@ -20,15 +32,6 @@
opacity: 1; opacity: 1;
} }
} }
> *:not(:last-child) {
margin-bottom: 24px;
}
.separator {
width: 100%;
border: 1px solid var(--color-neutral-200);
}
} }
.background { .background {

View File

@ -2,8 +2,8 @@ import LogOutButton from "@Front/Components/DesignSystem/LogOutButton";
import Module from "@Front/Config/Module"; import Module from "@Front/Config/Module";
import React from "react"; import React from "react";
import NavigationLink from "../../NavigationLink";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import MenuItem from "@Front/Components/DesignSystem/Menu/MenuItem";
type IProps = { type IProps = {
isOpen: boolean; isOpen: boolean;
@ -19,10 +19,28 @@ export default class ProfileModal extends React.Component<IProps, IState> {
<> <>
<div className={classes["background"]} onClick={this.props.closeModal} /> <div className={classes["background"]} onClick={this.props.closeModal} />
<div className={classes["root"]}> <div className={classes["root"]}>
<NavigationLink path={Module.getInstance().get().modules.pages.MyAccount.props.path} text="Mon compte" /> <MenuItem
<NavigationLink target="_blank" path="https://ressources.lecoffre.io/" text="Guide de Prise en Main" /> item={{
<NavigationLink target="_blank" path="/CGU_LeCoffre_io.pdf" text="CGU" /> text: "Mon compte",
<div className={classes["separator"]} /> link: Module.getInstance().get().modules.pages.MyAccount.props.path,
}}
/>
<MenuItem
item={{
text: "Guide de Prise en Main",
link: "https://ressources.lecoffre.io/",
target: "_blank",
}}
/>
<MenuItem
item={{
text: "CGU",
link: "/CGU_LeCoffre_io.pdf",
hasSeparator: true,
}}
/>
<LogOutButton /> <LogOutButton />
</div> </div>
</> </>

View File

@ -1,27 +1,20 @@
import React from "react";
import Image from "next/image";
import DisconnectIcon from "@Assets/Icons/disconnect.svg";
import classes from "./classes.module.scss";
import Typography, { ETypo, ETypoColor } from "../Typography";
import { useRouter } from "next/router";
import UserStore from "@Front/Stores/UserStore";
import { FrontendVariables } from "@Front/Config/VariablesFront"; import { FrontendVariables } from "@Front/Config/VariablesFront";
import UserStore from "@Front/Stores/UserStore";
import { PowerIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import MenuItem from "../Menu/MenuItem";
export default function LogOut() { export default function LogOut() {
const router = useRouter(); const router = useRouter();
const variables = FrontendVariables.getInstance(); const variables = FrontendVariables.getInstance();
const disconnect = async () => { const disconnect = useCallback(() => {
await UserStore.instance.disconnect(); UserStore.instance
router.push(`https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=${variables.FRONT_APP_HOST}`); .disconnect()
}; .then(() => router.push(`https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=${variables.FRONT_APP_HOST}`));
}, [router, variables.FRONT_APP_HOST]);
return ( return <MenuItem item={{ text: "Déconnexion", icon: <PowerIcon />, onClick: disconnect }} />;
<div className={classes["root"]} onClick={disconnect}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
Déconnexion
</Typography>
<Image src={DisconnectIcon} className={classes["disconnect-icon"]} alt="disconnect" />
</div>
);
} }

View File

@ -1,5 +1,6 @@
.root { .root {
width: 100%; width: 100%;
.menu-item { .menu-item {
display: flex; display: flex;
padding: var(--spacing-md, 16px); padding: var(--spacing-md, 16px);
@ -7,12 +8,16 @@
align-items: center; align-items: center;
gap: var(--spacing-lg, 24px); gap: var(--spacing-lg, 24px);
cursor: pointer; cursor: pointer;
}
> svg { svg {
width: 24px; width: 24px;
height: 24px; height: 24px;
transition: all ease-in-out 0.1s; transition: transform 0.3s ease-in-out;
} }
.chevron.open {
transform: rotate(180deg);
} }
.separator { .separator {
@ -20,4 +25,14 @@
height: 1px; height: 1px;
background-color: var(--separator-stroke-light, #d7dce0); background-color: var(--separator-stroke-light, #d7dce0);
} }
.dropdown {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
}
.dropdown.open {
max-height: 500px;
}
} }

View File

@ -3,27 +3,71 @@ import classes from "./classes.module.scss";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import useHoverable from "@Front/Hooks/useHoverable"; import useHoverable from "@Front/Hooks/useHoverable";
import { IItem } from "..";
import classNames from "classnames"; import classNames from "classnames";
import { IAppRule } from "@Front/Api/Entities/rule";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import useOpenable from "@Front/Hooks/useOpenable";
type IProps = { type IProps = {
item: IItem; item: IItem;
closeMenuCb: () => void;
}; };
type IItemBase = {
text: string;
icon?: JSX.Element;
hasSeparator?: boolean;
color?: ETypoColor;
onClose?: () => void;
};
type IItemWithLink = IItemBase & {
link: string;
rules?: IAppRule[];
routesActive?: string[];
onClick?: never;
dropdown?: never;
target?: "_blank";
};
type IItemWithOnClick = IItemBase & {
onClick: () => void;
link?: never;
rules?: never;
routesActive?: never;
dropdown?: never;
target?: never;
};
type IItemWithDropdown = IItemBase & {
dropdown: {
items: IItem[];
};
routesActive?: never;
link?: never;
rules?: never;
onClick?: never;
target?: never;
};
export type IItem = IItemWithLink | IItemWithOnClick | IItemWithDropdown;
export default function MenuItem(props: IProps) { export default function MenuItem(props: IProps) {
const { item, closeMenuCb } = props; const { item } = props;
const router = useRouter(); const router = useRouter();
const { pathname } = router; const { pathname } = router;
const [isActive, setIsActive] = React.useState(item.link === pathname); const [isActive, setIsActive] = React.useState(item.link === pathname);
const { isOpen, toggle, open } = useOpenable();
const handleClickElement = useCallback( const handleClickElement = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => { (e: React.MouseEvent<HTMLDivElement>) => {
closeMenuCb(); item.onClose?.();
const link = e.currentTarget.getAttribute("data-link"); const link = e.currentTarget.getAttribute("data-link");
if (item.target === "_blank") window.open(item.link, "_blank");
if (link) router.push(link); if (link) router.push(link);
if (item.onClick) item.onClick(); if (item.onClick) item.onClick();
}, },
[closeMenuCb, item, router], [item, router],
); );
const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable(); const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable();
@ -44,7 +88,25 @@ export default function MenuItem(props: IProps) {
if (pathname.includes(routeActive)) setIsActive(true); if (pathname.includes(routeActive)) setIsActive(true);
} }
} }
}, [isActive, item.link, item.routesActive, pathname]); if (item.dropdown) {
for (const subItem of item.dropdown.items) {
if (isActive) break;
if (subItem.link === pathname) {
!isOpen && open();
setIsActive(true);
}
if (subItem.routesActive) {
for (const routeActive of subItem.routesActive) {
if (isActive) break;
if (pathname.includes(routeActive)) {
!isOpen && open();
setIsActive(true);
}
}
}
}
}
}, [isActive, isOpen, item.dropdown, item.link, item.routesActive, open, pathname]);
return ( return (
<div <div
@ -53,12 +115,23 @@ export default function MenuItem(props: IProps) {
data-link={item.link} data-link={item.link}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}> onMouseLeave={handleMouseLeave}>
<div className={classes["menu-item"]}> <div className={classes["menu-item"]} onClick={item.dropdown && toggle}>
{React.cloneElement(item.icon, { color: `var(${getColor()})` })} {item.icon && React.cloneElement(item.icon, { color: `var(${getColor()})` })}
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}> <Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
{item.text} {item.text}
</Typography> </Typography>
{item.dropdown &&
React.cloneElement(<ChevronDownIcon className={classNames(classes["chevron"], isOpen && [classes["open"]])} />, {
color: `var(${getColor()})`,
})}
</div> </div>
{item.dropdown && (
<div className={classNames(classes["dropdown"], isOpen && [classes["open"]])}>
{item.dropdown.items.map((subItem, index) => (
<MenuItem key={index} item={subItem} />
))}
</div>
)}
{item.hasSeparator && <div className={classes["separator"]} />} {item.hasSeparator && <div className={classes["separator"]} />}
</div> </div>
); );

View File

@ -1,35 +1,10 @@
import { IAppRule } from "@Front/Api/Entities/rule";
import { ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import useHoverable from "@Front/Hooks/useHoverable"; import useHoverable from "@Front/Hooks/useHoverable";
import useOpenable from "@Front/Hooks/useOpenable"; import useOpenable from "@Front/Hooks/useOpenable";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import MenuItem from "./MenuItem"; import MenuItem, { IItem } 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 = { type IProps = {
children: React.ReactNode; children: React.ReactNode;
@ -79,7 +54,7 @@ export default function Menu(props: IProps) {
{items.map((item, index) => { {items.map((item, index) => {
return ( return (
<Rules mode={RulesMode.NECESSARY} rules={item.rules ?? []} key={item.link}> <Rules mode={RulesMode.NECESSARY} rules={item.rules ?? []} key={item.link}>
<MenuItem item={item} key={index} closeMenuCb={close} /> <MenuItem item={item} key={index} />
</Rules> </Rules>
); );
})} })}

View File

@ -1,6 +1,5 @@
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; 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";
@ -10,6 +9,8 @@ import { useCallback } from "react";
import { AnchorStatus } from ".."; import { AnchorStatus } from "..";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import { IItem } from "@Front/Components/DesignSystem/Menu/MenuItem";
import Menu from "@Front/Components/DesignSystem/Menu";
type IProps = { type IProps = {
folder: OfficeFolder | null; folder: OfficeFolder | null;