From 8cbd9da9bbb4dbb8f8ef90d8fdf00eb525608393 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Tue, 23 Jul 2024 18:05:26 +0200 Subject: [PATCH] :sparkles: MEP-PPD --- src/front/Assets/Icons/notif-empty.svg | 3 + .../Assets/images/background_refonte.svg | 16 + .../images/background_refonte_reverse.svg | 22 + src/front/Assets/logo_small_blue.svg | 4 + src/front/Assets/logo_small_neutral.svg | 4 + src/front/Assets/logo_standard_blue.svg | 15 + src/front/Assets/logo_standard_neutral.svg | 15 + .../DesignSystem/Alert/classes.module.scss | 91 ++ .../Components/DesignSystem/Alert/index.tsx | 81 ++ .../CircleProgress/classes.module.scss | 24 + .../DesignSystem/CircleProgress/index.tsx | 62 ++ .../EmptyAlert/classes.module.scss | 28 + .../DesignSystem/EmptyAlert/index.tsx | 30 + .../IconButton/classes.module.scss | 101 +++ .../DesignSystem/IconButton/index.tsx | 32 + .../OldModal/Alert/classes.module.scss | 22 + .../DesignSystem/OldModal/Alert/index.tsx | 54 ++ .../OldModal/Confirm/classes.module.scss | 22 + .../DesignSystem/OldModal/Confirm/index.tsx | 73 ++ .../Elements/Footer/classes.module.scss | 5 + .../OldModal/Elements/Footer/index.tsx | 12 + .../Elements/Header/classes.module.scss | 12 + .../OldModal/Elements/Header/index.tsx | 17 + .../Elements/Loader/classes.module.scss | 13 + .../OldModal/Elements/Loader/index.tsx | 20 + .../DesignSystem/OldModal/classes.module.scss | 125 +++ .../DesignSystem/OldModal/index.tsx | 111 +++ .../Table/MuiTable/classes.module.scss | 38 + .../DesignSystem/Table/MuiTable/index.tsx | 102 +++ .../Table/SearchBar/classes.module.scss | 40 + .../DesignSystem/Table/SearchBar/index.tsx | 33 + .../DesignSystem/Table/classes.module.scss | 13 + .../Components/DesignSystem/Table/index.tsx | 45 + .../DesignSystem/Tag/classes.module.scss | 27 + .../Components/DesignSystem/Tag/index.tsx | 48 ++ .../SubMenuItem/classes.module.scss | 23 + .../ButtonWithSubMenu/SubMenuItem/index.tsx | 50 ++ .../ButtonWithSubMenu/classes.module.scss | 29 + .../Elements/ButtonWithSubMenu/index.tsx | 78 ++ .../Elements/InfiniteScroll/index.tsx | 105 +++ .../Tabs/HorizontalTab/classes.module.scss | 12 + .../Elements/Tabs/HorizontalTab/index.tsx | 34 + .../Tabs/VerticalTabs/classes.module.scss | 4 + .../Elements/Tabs/VerticalTabs/index.tsx | 24 + .../Elements/Tabs/classes.module.scss | 50 ++ src/front/Components/Elements/Tabs/index.tsx | 142 ++++ .../Layouts/DesignSystem/classes.module.scss | 24 + .../Components/Layouts/DesignSystem/index.tsx | 803 ++++++++++++++++++ .../ClientBox/DeleteCustomerModal/index.tsx | 33 + .../ClientView/ClientBox/classes.module.scss | 22 + .../ClientView/ClientBox/index.tsx | 141 +++ .../DeleteAskedDocumentModal/index.tsx | 37 + .../DocumentTables/FilePreviewModal/index.tsx | 20 + .../DocumentTables/classes.module.scss | 14 + .../ClientView/DocumentTables/index.tsx | 226 +++++ .../ClientView/NoDocument/classes.module.scss | 7 + .../ClientView/NoDocument/index.tsx | 20 + .../ClientView/classes.module.scss | 30 + .../FolderInformation/ClientView/index.tsx | 115 +++ .../InformationSection/classes.module.scss | 50 ++ .../InformationSection/index.tsx | 103 +++ .../NoClientView/AddClientSection/index.tsx | 34 + .../NoClientView/DeleteFolderModal/index.tsx | 42 + .../NoClientView/classes.module.scss | 11 + .../FolderInformation/NoClientView/index.tsx | 42 + .../elements/AnchoringAlertInfo/index.tsx | 25 + .../elements/AnchoringAlertSuccess/index.tsx | 36 + .../AnchoringModal/classes.module.scss | 11 + .../elements/AnchoringModal/index.tsx | 62 ++ .../AnchoringProcessingInfo/index.tsx | 14 + .../elements/ArchiveAlertWarning/index.tsx | 48 ++ .../elements/ArchiveModal/classes.module.scss | 5 + .../elements/ArchiveModal/index.tsx | 48 ++ .../DownloadAnchoringProofModal/index.tsx | 47 + .../RequireAnchoringModal/classes.module.scss | 11 + .../elements/RequireAnchoringModal/index.tsx | 32 + .../PasswordForgotten/classes.module.scss | 29 + .../Layouts/Login/PasswordForgotten/index.tsx | 43 + .../Login/StepEmail/classes.module.scss | 32 + .../Layouts/Login/StepEmail/index.tsx | 194 +++++ .../Login/StepNewPassword/classes.module.scss | 29 + .../Layouts/Login/StepNewPassword/index.tsx | 43 + .../Login/StepPassword/classes.module.scss | 30 + .../Layouts/Login/StepPassword/index.tsx | 70 ++ .../Login/StepTotp/classes.module.scss | 43 + .../Layouts/Login/StepTotp/index.tsx | 77 ++ .../Layouts/Login/france-connect.svg | 9 + src/front/Themes/constants-old.scss | 44 + src/front/Themes/variables-old.scss | 36 + src/pages/design-system/index.tsx | 5 + 90 files changed, 4708 insertions(+) create mode 100644 src/front/Assets/Icons/notif-empty.svg create mode 100644 src/front/Assets/images/background_refonte.svg create mode 100644 src/front/Assets/images/background_refonte_reverse.svg create mode 100644 src/front/Assets/logo_small_blue.svg create mode 100644 src/front/Assets/logo_small_neutral.svg create mode 100644 src/front/Assets/logo_standard_blue.svg create mode 100644 src/front/Assets/logo_standard_neutral.svg create mode 100644 src/front/Components/DesignSystem/Alert/classes.module.scss create mode 100644 src/front/Components/DesignSystem/Alert/index.tsx create mode 100644 src/front/Components/DesignSystem/CircleProgress/classes.module.scss create mode 100644 src/front/Components/DesignSystem/CircleProgress/index.tsx create mode 100644 src/front/Components/DesignSystem/EmptyAlert/classes.module.scss create mode 100644 src/front/Components/DesignSystem/EmptyAlert/index.tsx create mode 100644 src/front/Components/DesignSystem/IconButton/classes.module.scss create mode 100644 src/front/Components/DesignSystem/IconButton/index.tsx create mode 100644 src/front/Components/DesignSystem/OldModal/Alert/classes.module.scss create mode 100644 src/front/Components/DesignSystem/OldModal/Alert/index.tsx create mode 100644 src/front/Components/DesignSystem/OldModal/Confirm/classes.module.scss create mode 100644 src/front/Components/DesignSystem/OldModal/Confirm/index.tsx create mode 100644 src/front/Components/DesignSystem/OldModal/Elements/Footer/classes.module.scss create mode 100644 src/front/Components/DesignSystem/OldModal/Elements/Footer/index.tsx create mode 100644 src/front/Components/DesignSystem/OldModal/Elements/Header/classes.module.scss create mode 100644 src/front/Components/DesignSystem/OldModal/Elements/Header/index.tsx create mode 100644 src/front/Components/DesignSystem/OldModal/Elements/Loader/classes.module.scss create mode 100644 src/front/Components/DesignSystem/OldModal/Elements/Loader/index.tsx create mode 100644 src/front/Components/DesignSystem/OldModal/classes.module.scss create mode 100644 src/front/Components/DesignSystem/OldModal/index.tsx create mode 100644 src/front/Components/DesignSystem/Table/MuiTable/classes.module.scss create mode 100644 src/front/Components/DesignSystem/Table/MuiTable/index.tsx create mode 100644 src/front/Components/DesignSystem/Table/SearchBar/classes.module.scss create mode 100644 src/front/Components/DesignSystem/Table/SearchBar/index.tsx create mode 100644 src/front/Components/DesignSystem/Table/classes.module.scss create mode 100644 src/front/Components/DesignSystem/Table/index.tsx create mode 100644 src/front/Components/DesignSystem/Tag/classes.module.scss create mode 100644 src/front/Components/DesignSystem/Tag/index.tsx create mode 100644 src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss create mode 100644 src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx create mode 100644 src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss create mode 100644 src/front/Components/Elements/ButtonWithSubMenu/index.tsx create mode 100644 src/front/Components/Elements/InfiniteScroll/index.tsx create mode 100644 src/front/Components/Elements/Tabs/HorizontalTab/classes.module.scss create mode 100644 src/front/Components/Elements/Tabs/HorizontalTab/index.tsx create mode 100644 src/front/Components/Elements/Tabs/VerticalTabs/classes.module.scss create mode 100644 src/front/Components/Elements/Tabs/VerticalTabs/index.tsx create mode 100644 src/front/Components/Elements/Tabs/classes.module.scss create mode 100644 src/front/Components/Elements/Tabs/index.tsx create mode 100644 src/front/Components/Layouts/DesignSystem/classes.module.scss create mode 100644 src/front/Components/Layouts/DesignSystem/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/InformationSection/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/NoClientView/AddClientSection/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/NoClientView/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/NoClientView/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringProcessingInfo/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveAlertWarning/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss create mode 100644 src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx create mode 100644 src/front/Components/Layouts/Login/PasswordForgotten/classes.module.scss create mode 100644 src/front/Components/Layouts/Login/PasswordForgotten/index.tsx create mode 100644 src/front/Components/Layouts/Login/StepEmail/classes.module.scss create mode 100644 src/front/Components/Layouts/Login/StepEmail/index.tsx create mode 100644 src/front/Components/Layouts/Login/StepNewPassword/classes.module.scss create mode 100644 src/front/Components/Layouts/Login/StepNewPassword/index.tsx create mode 100644 src/front/Components/Layouts/Login/StepPassword/classes.module.scss create mode 100644 src/front/Components/Layouts/Login/StepPassword/index.tsx create mode 100644 src/front/Components/Layouts/Login/StepTotp/classes.module.scss create mode 100644 src/front/Components/Layouts/Login/StepTotp/index.tsx create mode 100644 src/front/Components/Layouts/Login/france-connect.svg create mode 100644 src/front/Themes/constants-old.scss create mode 100644 src/front/Themes/variables-old.scss create mode 100644 src/pages/design-system/index.tsx diff --git a/src/front/Assets/Icons/notif-empty.svg b/src/front/Assets/Icons/notif-empty.svg new file mode 100644 index 00000000..ad6a4f2f --- /dev/null +++ b/src/front/Assets/Icons/notif-empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/front/Assets/images/background_refonte.svg b/src/front/Assets/images/background_refonte.svg new file mode 100644 index 00000000..6c624b5c --- /dev/null +++ b/src/front/Assets/images/background_refonte.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/front/Assets/images/background_refonte_reverse.svg b/src/front/Assets/images/background_refonte_reverse.svg new file mode 100644 index 00000000..1d862495 --- /dev/null +++ b/src/front/Assets/images/background_refonte_reverse.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/Assets/logo_small_blue.svg b/src/front/Assets/logo_small_blue.svg new file mode 100644 index 00000000..d3ac57f9 --- /dev/null +++ b/src/front/Assets/logo_small_blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/front/Assets/logo_small_neutral.svg b/src/front/Assets/logo_small_neutral.svg new file mode 100644 index 00000000..9935033b --- /dev/null +++ b/src/front/Assets/logo_small_neutral.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/front/Assets/logo_standard_blue.svg b/src/front/Assets/logo_standard_blue.svg new file mode 100644 index 00000000..a4af4bde --- /dev/null +++ b/src/front/Assets/logo_standard_blue.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/front/Assets/logo_standard_neutral.svg b/src/front/Assets/logo_standard_neutral.svg new file mode 100644 index 00000000..242b01dc --- /dev/null +++ b/src/front/Assets/logo_standard_neutral.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/front/Components/DesignSystem/Alert/classes.module.scss b/src/front/Components/DesignSystem/Alert/classes.module.scss new file mode 100644 index 00000000..e800be15 --- /dev/null +++ b/src/front/Components/DesignSystem/Alert/classes.module.scss @@ -0,0 +1,91 @@ +@import "@Themes/constants.scss"; + +.root { + width: fit-content; + display: inline-flex; + padding: var(--spacing-2, 16px); + align-items: center; + gap: var(--spacing-lg, 24px); + + border-radius: var(--alerts-radius, 0px); + border: 1px solid var(--alerts-info-border); + background: var(--alerts-info-background); + + .content { + display: flex; + flex-direction: column; + gap: var(--spacing-md, 16px); + + .text-container { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + } + + .button-container { + display: flex; + gap: var(--spacing-md, 16px); + } + } + + .icon { + display: flex; + padding: var(--spacing-1, 8px); + align-items: center; + align-self: flex-start; + + border-radius: var(--alerts-badge-radius, 360px); + border: 1px solid var(--alerts-badge-border, rgba(0, 0, 0, 0)); + background: var(--alerts-badge-background, #fff); + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1); + + svg { + width: 24px; + height: 24px; + min-width: 24px; + min-height: 24px; + + stroke: var(--alerts-badge-contrast-info); + } + } + + &.error { + border-color: var(--alerts-error-border); + background: var(--alerts-error-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-error); + } + } + + &.warning { + border-color: var(--alerts-warning-border); + background: var(--alerts-warning-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-warning); + } + } + + &.success { + border-color: var(--alerts-success-border); + background: var(--alerts-success-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-success); + } + } + + &.neutral { + border-color: var(--alerts-neutral-border); + background: var(--alerts-neutral-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-neutral); + } + } + + &.fullwidth { + width: 100%; + } +} diff --git a/src/front/Components/DesignSystem/Alert/index.tsx b/src/front/Components/DesignSystem/Alert/index.tsx new file mode 100644 index 00000000..a00022bc --- /dev/null +++ b/src/front/Components/DesignSystem/Alert/index.tsx @@ -0,0 +1,81 @@ +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import React from "react"; +import { InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/outline"; + +import classes from "./classes.module.scss"; +import Button, { EButtonSize, EButtonstyletype, EButtonVariant, IButtonProps } from "../Button"; +import classNames from "classnames"; +import IconButton from "../IconButton"; +import useOpenable from "@Front/Hooks/useOpenable"; + +type IProps = { + variant: EAlertVariant; + title: string; + description: string; + icon?: React.ReactNode; + firstButton?: IButtonProps; + secondButton?: IButtonProps; + closeButton?: boolean; + fullWidth?: boolean; +}; + +export enum EAlertVariant { + INFO = "info", + SUCCESS = "success", + WARNING = "warning", + ERROR = "error", + NEUTRAL = "neutral", +} + +const variantButtonMap: Record = { + [EAlertVariant.INFO]: EButtonVariant.PRIMARY, + [EAlertVariant.SUCCESS]: EButtonVariant.SUCCESS, + [EAlertVariant.WARNING]: EButtonVariant.WARNING, + [EAlertVariant.ERROR]: EButtonVariant.ERROR, + [EAlertVariant.NEUTRAL]: EButtonVariant.NEUTRAL, +}; + +export default function Alert(props: IProps) { + const { isOpen, close } = useOpenable({ defaultOpen: true }); + const { variant = EAlertVariant.INFO, title, description, firstButton, secondButton, closeButton, icon, fullWidth } = props; + + if (!isOpen) return null; + + return ( +
+ {icon ?? } +
+
+ + {title} + + + {description} + +
+ +
+ {firstButton && ( + + )} + {secondButton && ( + + )} +
+
+ {closeButton && } />} +
+ ); +} diff --git a/src/front/Components/DesignSystem/CircleProgress/classes.module.scss b/src/front/Components/DesignSystem/CircleProgress/classes.module.scss new file mode 100644 index 00000000..0ad37f7c --- /dev/null +++ b/src/front/Components/DesignSystem/CircleProgress/classes.module.scss @@ -0,0 +1,24 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + align-items: center; + gap: var(--spacing-sm); + + svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); + + .circleBackground { + fill: none; + stroke: var(--progress-circle-background); + } + + .circleProgress { + fill: none; + stroke: var(--progress-circle-contrast); + transition: stroke-dashoffset 0.35s; + } + } +} diff --git a/src/front/Components/DesignSystem/CircleProgress/index.tsx b/src/front/Components/DesignSystem/CircleProgress/index.tsx new file mode 100644 index 00000000..4b66e985 --- /dev/null +++ b/src/front/Components/DesignSystem/CircleProgress/index.tsx @@ -0,0 +1,62 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import Typography, { ETypo, ETypoColor } from "../Typography"; +import classes from "./classes.module.scss"; + +type IProps = { + percentage: number; +}; + +export default function CircleProgress(props: IProps) { + const { percentage } = props; + + const [animatedProgress, setAnimatedProgress] = useState(0); + const requestRef = useRef(); + + const animate = useCallback(() => { + setAnimatedProgress((prev) => { + if (prev < percentage) { + return prev + 1; + } else { + if (requestRef.current) { + cancelAnimationFrame(requestRef.current); + } + return prev; + } + }); + requestRef.current = requestAnimationFrame(animate); + }, [percentage]); + + useEffect(() => { + setAnimatedProgress(0); // Reset progress + requestRef.current = requestAnimationFrame(animate); + return () => { + if (requestRef.current) { + cancelAnimationFrame(requestRef.current); + } + }; + }, [percentage, animate]); + + const radius = 11; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (animatedProgress / 100) * circumference; + + return ( +
+ + + + + + {Math.round(percentage)}% + +
+ ); +} \ No newline at end of file diff --git a/src/front/Components/DesignSystem/EmptyAlert/classes.module.scss b/src/front/Components/DesignSystem/EmptyAlert/classes.module.scss new file mode 100644 index 00000000..0b036bd3 --- /dev/null +++ b/src/front/Components/DesignSystem/EmptyAlert/classes.module.scss @@ -0,0 +1,28 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: var(--spacing-3, 24px); + gap: var(--spacing-lg, 24px); + + border-radius: var(--radius-minimal, 8px); + background: var(--primary-weak-higlight, #e5eefa); + + text-align: center; + + .text { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--spacing-md, 16px); + } + + svg { + width: 32px; + stroke: var(--primary-weak-contrast); + } +} diff --git a/src/front/Components/DesignSystem/EmptyAlert/index.tsx b/src/front/Components/DesignSystem/EmptyAlert/index.tsx new file mode 100644 index 00000000..17b3e0c1 --- /dev/null +++ b/src/front/Components/DesignSystem/EmptyAlert/index.tsx @@ -0,0 +1,30 @@ +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import React from "react"; + +import classes from "./classes.module.scss"; + +type IProps = { + icon: React.ReactNode; + title: string; + description: string; + footer?: React.ReactNode; +}; + +export default function EmptyAlert(props: IProps) { + const { icon, title, description, footer } = props; + + return ( +
+ {icon} +
+ + {title} + + + {description} + +
+ {footer} +
+ ); +} diff --git a/src/front/Components/DesignSystem/IconButton/classes.module.scss b/src/front/Components/DesignSystem/IconButton/classes.module.scss new file mode 100644 index 00000000..920f424e --- /dev/null +++ b/src/front/Components/DesignSystem/IconButton/classes.module.scss @@ -0,0 +1,101 @@ +@import "@Themes/constants.scss"; + +.root { + cursor: pointer; + border-radius: var(--button-icon-button-radius, 8px); + display: inline-flex; + padding: var(--spacing-sm, 8px); + align-items: flex-start; + + svg { + width: 24px; + min-width: 24px; + stroke: var(--button-icon-button-default-default); + } + + &:hover { + svg { + stroke: var(--button-icon-button-default-hovered); + } + } + + &.neutral { + background: var(--button-icon-button-neutral-default); + + svg { + stroke: var(--button-icon-button-default-default); + } + + &:hover { + background: var(--button-icon-button-neutral-hovered); + + svg { + stroke: var(--button-icon-button-default-hovered); + } + } + } + + &.primary { + background: var(--button-icon-button-primary-default); + + svg { + stroke: var(--button-icon-button-primary-contrast); + } + + &:hover { + background: var(--button-icon-button-primary-hovered); + } + } + + &.error { + background: var(--button-icon-button-error-default); + + svg { + stroke: var(--button-icon-button-error-contrast); + } + + &:hover { + background: var(--button-icon-button-error-hovered); + } + } + &.success { + background: var(--button-icon-button-success-default); + + svg { + stroke: var(--button-icon-button-success-contrast); + } + + &:hover { + background: var(--button-icon-button-success-hovered); + } + } + + &.warning { + background: var(--button-icon-button-warning-default); + + svg { + stroke: var(--button-icon-button-warning-contrast); + } + + &:hover { + background: var(--button-icon-button-warning-hovered); + } + } + + &.info { + background: var(--button-icon-button-info-default); + + svg { + stroke: var(--button-icon-button-info-contrast); + } + + &:hover { + background: var(--button-icon-button-info-hovered); + } + } + + &.disabled { + cursor: default; + opacity: var(--opacity-disabled, 0.3); + } +} diff --git a/src/front/Components/DesignSystem/IconButton/index.tsx b/src/front/Components/DesignSystem/IconButton/index.tsx new file mode 100644 index 00000000..817cee68 --- /dev/null +++ b/src/front/Components/DesignSystem/IconButton/index.tsx @@ -0,0 +1,32 @@ +import classNames from "classnames"; +import React from "react"; + +import classes from "./classes.module.scss"; + +export enum EIconButtonVariant { + DEFAULT = "default", + NEUTRAL = "neutral", + PRIMARY = "primary", + ERROR = "error", + SUCCESS = "success", + WARNING = "warning", + INFO = "info", +} + +type IProps = { + icon: React.ReactNode; + onClick?: React.MouseEventHandler | undefined; + variant?: EIconButtonVariant; + disabled?: boolean; + className?: string; +}; + +export default function IconButton(props: IProps) { + const { icon, onClick, className, variant = EIconButtonVariant.DEFAULT, disabled = false } = props; + + return ( + + {icon} + + ); +} diff --git a/src/front/Components/DesignSystem/OldModal/Alert/classes.module.scss b/src/front/Components/DesignSystem/OldModal/Alert/classes.module.scss new file mode 100644 index 00000000..43df3130 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Alert/classes.module.scss @@ -0,0 +1,22 @@ +@import "@Themes/constants.scss"; + +.button-container { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 8px; + + button { + flex: 1; + } + + .sub-container { + flex: 1; + display: flex; + } + + @media (max-width: $screen-s) { + flex-direction: column-reverse; + gap: 8px; + } +} \ No newline at end of file diff --git a/src/front/Components/DesignSystem/OldModal/Alert/index.tsx b/src/front/Components/DesignSystem/OldModal/Alert/index.tsx new file mode 100644 index 00000000..d9293647 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Alert/index.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import OldModal, { IProps as IPropsModal } from ".."; +import Button, { EButtonVariant } from "../../Button"; +import classes from "./classes.module.scss"; + +type IProps = IPropsModal & { + closeText: string | JSX.Element; +}; + +type IState = { + isOpen: boolean; +}; +export default class Alert extends React.Component { + static defaultProps = { + closeText: "Ok", + ...OldModal.defaultProps, + }; + + constructor(props: IProps) { + super(props); + this.state = { + isOpen: this.props.isOpen ?? true, + }; + this.onClose = this.onClose.bind(this); + } + + public override render(): JSX.Element | null { + return ( + + {this.props.children} + + ); + } + + private footer(): JSX.Element { + return ( +
+ +
+ ); + } + + private onClose() { + this.setState({ isOpen: false }); + this.props.onClose?.(); + } +} diff --git a/src/front/Components/DesignSystem/OldModal/Confirm/classes.module.scss b/src/front/Components/DesignSystem/OldModal/Confirm/classes.module.scss new file mode 100644 index 00000000..95816c02 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Confirm/classes.module.scss @@ -0,0 +1,22 @@ +@import "@Themes/constants.scss"; + +.buttons-container { + display: flex; + justify-content: space-between; + gap: 16px; + margin-top: 8px; + + button { + flex: 1; + } + + .sub-container { + flex: 1; + display: flex; + } + + @media (max-width: $screen-s) { + flex-direction: column-reverse; + gap: 8px; + } +} \ No newline at end of file diff --git a/src/front/Components/DesignSystem/OldModal/Confirm/index.tsx b/src/front/Components/DesignSystem/OldModal/Confirm/index.tsx new file mode 100644 index 00000000..d7ebe82a --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Confirm/index.tsx @@ -0,0 +1,73 @@ +import Link from "next/link"; +import React from "react"; + +import OldModal, { IProps as IPropsModal } from ".."; +import Button, { EButtonstyletype, EButtonVariant } from "../../Button"; +import classes from "./classes.module.scss"; + +type IProps = IPropsModal & { + onAccept?: () => void; + cancelText: string | JSX.Element; + cancelPath?: string; + confirmText: string | JSX.Element; + showCancelButton: boolean; + showButtons: boolean; + canConfirm: boolean; +}; + +type IState = {}; + +export default class Confirm extends React.Component { + static defaultProps = { + showCancelButton: true, + cancelText: "Cancel", + confirmText: "Confirm", + canConfirm: true, + showButtons: true, + ...OldModal.defaultProps, + }; + + public override render(): JSX.Element | null { + return ( + + {this.props.children} + + ); + } + + private footer(): JSX.Element | null { + if (!this.props.showButtons) return null; + return ( +
+ {this.props.showCancelButton && + (this.props.cancelPath ? ( + + + + ) : ( +
+ +
+ ))} +
+ +
+
+ ); + } +} diff --git a/src/front/Components/DesignSystem/OldModal/Elements/Footer/classes.module.scss b/src/front/Components/DesignSystem/OldModal/Elements/Footer/classes.module.scss new file mode 100644 index 00000000..1009a115 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Elements/Footer/classes.module.scss @@ -0,0 +1,5 @@ +@import "@Themes/constants.scss"; + +.root { + margin-top: 24px; +} diff --git a/src/front/Components/DesignSystem/OldModal/Elements/Footer/index.tsx b/src/front/Components/DesignSystem/OldModal/Elements/Footer/index.tsx new file mode 100644 index 00000000..1b166f40 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Elements/Footer/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classes from "./classes.module.scss"; + +type IProps = { + content: JSX.Element; +}; + +export default class Footer extends React.Component { + public override render(): JSX.Element { + return
{this.props.content}
; + } +} diff --git a/src/front/Components/DesignSystem/OldModal/Elements/Header/classes.module.scss b/src/front/Components/DesignSystem/OldModal/Elements/Header/classes.module.scss new file mode 100644 index 00000000..4a2a22bd --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Elements/Header/classes.module.scss @@ -0,0 +1,12 @@ +@import "@Themes/constants.scss"; + +.root { + margin-bottom: 24px; + display: flex; + align-items: center; + justify-content: flex-start; + + h5 { + color: var(--color-neutral-900); + } +} diff --git a/src/front/Components/DesignSystem/OldModal/Elements/Header/index.tsx b/src/front/Components/DesignSystem/OldModal/Elements/Header/index.tsx new file mode 100644 index 00000000..c8373019 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Elements/Header/index.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; + +type IProps = { + content: string | JSX.Element; +}; + +export default class Header extends React.Component { + public override render(): JSX.Element { + return ( +
+ {this.props.content} +
+ ); + } +} diff --git a/src/front/Components/DesignSystem/OldModal/Elements/Loader/classes.module.scss b/src/front/Components/DesignSystem/OldModal/Elements/Loader/classes.module.scss new file mode 100644 index 00000000..687007f0 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Elements/Loader/classes.module.scss @@ -0,0 +1,13 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + margin: 0; + padding: 40px; + margin-top: 24px; +} diff --git a/src/front/Components/DesignSystem/OldModal/Elements/Loader/index.tsx b/src/front/Components/DesignSystem/OldModal/Elements/Loader/index.tsx new file mode 100644 index 00000000..60d94920 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/Elements/Loader/index.tsx @@ -0,0 +1,20 @@ +// import Loader from "Components/Elements/Loader"; +import React from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; + +type IProps = { + text?: string | JSX.Element; +}; + +export default class PopUpLoader extends React.Component { + public override render(): JSX.Element { + return ( +
+ {/* */} + TODO: INTEGRER LOARDER ISLOADING + {this.props.text && this.props.text} +
+ ); + } +} diff --git a/src/front/Components/DesignSystem/OldModal/classes.module.scss b/src/front/Components/DesignSystem/OldModal/classes.module.scss new file mode 100644 index 00000000..408155b3 --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/classes.module.scss @@ -0,0 +1,125 @@ +@import "@Themes/constants.scss"; + +@keyframes smooth-appear { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes smooth-disappear { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +.root { + position: fixed; + z-index: 6; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + --animation-delay: 1ms; + animation: smooth-appear var(--animation-delay) $custom-easing; + + &[data-will-close="true"] { + animation: smooth-disappear var(--animation-delay) $custom-easing; + opacity: 0; + } + + .background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: $modal-background; + } + + .container { + position: relative; + width: 610px; + max-height: 90%; + background: var(--color-generic-white); + box-shadow: 0px 6px 12px rgba(255, 255, 255, 0.11); + overflow: auto; + padding: 32px; + + @media (max-width: $screen-s) { + width: 90%; + max-width: 493px; + } + + .cross { + display: flex; + flex-direction: row-reverse; + + .close-icon { + height: 24px; + width: 24px; + cursor: pointer; + } + } + } + + .transparant-background { + background-color: transparent; + box-shadow: none; + } + + &[data-side-background="true"] { + .container { + max-width: 711px; + + .sub-container { + padding: 0; + display: flex; + + p { + max-width: 711px; + } + + @media (max-width: $screen-s) { + display: block; + } + + .banner { + @media (max-width: $screen-s) { + overflow: hidden; + } + } + } + } + + .side-image { + height: 100%; + + @media (max-width: $screen-s) { + display: none; + } + } + + .top-image { + @media (min-width: $screen-s) { + display: none; + } + + @media (max-width: $screen-s) { + width: 100%; + max-height: 82px; + min-height: 82px; + } + } + } +} diff --git a/src/front/Components/DesignSystem/OldModal/index.tsx b/src/front/Components/DesignSystem/OldModal/index.tsx new file mode 100644 index 00000000..2b30c00a --- /dev/null +++ b/src/front/Components/DesignSystem/OldModal/index.tsx @@ -0,0 +1,111 @@ +import CrossIcon from "@Assets/Icons/cross.svg"; +import Image from "next/image"; +import React from "react"; + +import Typography, { ETypo } from "../Typography"; +import classes from "./classes.module.scss"; +import Footer from "./Elements/Footer"; +import Header from "./Elements/Header"; +import Loader from "./Elements/Loader"; + +export type IProps = { + closeBtn?: boolean; + header?: string | JSX.Element; + footer?: JSX.Element | null; + textLoader?: string | JSX.Element; + isOpen: boolean; + onClose: () => void; + hasTransparentBackground?: boolean; + hasContainerClosable?: boolean; + withSideBackground?: boolean; + children?: React.ReactNode; + animationDelay?: number; +}; + +type IState = { + willClose: boolean; + isOpen: boolean; +}; + +export default class OldModal extends React.Component { + static defaultProps = { + animationDelay: 250, + }; + public rootRefElement = React.createRef(); + constructor(props: IProps) { + super(props); + this.close = this.close.bind(this); + + this.state = { + willClose: false, + isOpen: this.props.isOpen, + }; + } + + public override render(): JSX.Element | null { + if (!this.state.isOpen) return null; + return ( +
+
+
+ {this.props.closeBtn && ( +
+ Unplugged +
+ )} +
+ {this.props.header &&
} + + + <>{this.props.children ? this.props.children : } + + {this.props.children && this.props.footer &&
} +
+
+
+ ); + } + + public override componentDidUpdate(prevProps: IProps): void { + if (prevProps.isOpen !== this.props.isOpen && !this.props.isOpen) { + this.setState({ willClose: true }); + window.setTimeout(() => { + this.setState({ + isOpen: false, + willClose: false, + }); + }, this.props.animationDelay); + document.body.style.overflow = "auto"; + } + if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen) { + this.setState({ isOpen: true }); + document.body.style.overflow = "hidden"; + } + this.rootRefElement.current?.style.setProperty("--animation-delay", this.props.animationDelay!.toString().concat("ms")); + } + + public override componentDidMount(): void { + document.addEventListener("keydown", this.handleKeyDown); + } + + public override componentWillUnmount(): void { + document.body.style.overflow = "auto"; + document.removeEventListener("keydown", this.handleKeyDown); + } + + protected close() { + if (this.props.hasContainerClosable === false) return; + if (this.state.willClose) return; + this.props.onClose(); + } + + private handleKeyDown = (e: KeyboardEvent): void => { + if (e.key === "Escape" || e.key === "Esc") { + this.props.onClose(); + } + }; +} diff --git a/src/front/Components/DesignSystem/Table/MuiTable/classes.module.scss b/src/front/Components/DesignSystem/Table/MuiTable/classes.module.scss new file mode 100644 index 00000000..1348e460 --- /dev/null +++ b/src/front/Components/DesignSystem/Table/MuiTable/classes.module.scss @@ -0,0 +1,38 @@ +.root { + .head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + overflow: hidden; + + .text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + svg { + cursor: pointer; + min-width: 21px; + fill: var(--table-header-contrast); + } + } + + .cell { + overflow: hidden; + word-wrap: break-word; + + .content { + max-width: 270px; + width: 100%; + word-break: break-word; + } + } + + .row { + &:hover { + background-color: var(--table-background-hovered); + } + } +} diff --git a/src/front/Components/DesignSystem/Table/MuiTable/index.tsx b/src/front/Components/DesignSystem/Table/MuiTable/index.tsx new file mode 100644 index 00000000..83e05a7f --- /dev/null +++ b/src/front/Components/DesignSystem/Table/MuiTable/index.tsx @@ -0,0 +1,102 @@ +import InfiniteScroll from "@Front/Components/Elements/InfiniteScroll"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; + +import Typography, { ETypo, ETypoColor } from "../../Typography"; +import classes from "./classes.module.scss"; + +export type IRowProps = { key: string } & Record; + +type IRow = { + key?: string; + content: Record; +}; + +type IProps = { + header: readonly IHead[]; + rows: IRowProps[]; + onNext?: ((release: () => void, reset?: () => void) => Promise | void) | null; +}; + +export type IHead = { + key: string; + title?: string; +}; + +type CellContent = { + key: string; + value: React.ReactNode; +}; + +export default function MuiTable(props: IProps) { + const rows: IRow[] = props.rows.map((rowProps) => { + const row: IRow = { + key: rowProps.key, + content: {}, + }; + props.header.forEach((column) => { + const cellContent: CellContent = { + key: column.key + rowProps.key, + value: rowProps[column.key], + }; + row.content[column.key] = cellContent; + }); + return row; + }); + + return ( + + + + + + {props.header.map((column) => ( + + {column.title && ( + + + {column.title} + + {/* */} + + )} + + ))} + + + + {rows.map((row) => { + return ( + + {Object.values(row.content).map((cell) => ( + + + {cell.value} + + + ))} + + ); + })} + +
+
+
+ ); +} diff --git a/src/front/Components/DesignSystem/Table/SearchBar/classes.module.scss b/src/front/Components/DesignSystem/Table/SearchBar/classes.module.scss new file mode 100644 index 00000000..384a9a0a --- /dev/null +++ b/src/front/Components/DesignSystem/Table/SearchBar/classes.module.scss @@ -0,0 +1,40 @@ +.root { + display: flex; + position: relative; + align-items: center; + justify-content: space-between; + width: 100%; + border: 1px solid var(--Wild-Sand-100, #efefef); + background-color: var(--Wild-Sand-50, #f6f6f6); + box-sizing: border-box; + + .input-element { + padding: 10.5px 16px; + flex: 1; + border: 0; + background-color: transparent; + + &:focus, + input:focus { + outline: none; + } + + &::placeholder { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + } + + .icon { + position: absolute; + right: 10px; + width: 24px; + height: 24px; + pointer-events: none; + path { + stroke: var(--Wild-Sand-600, #bfbfbf); + } + } +} diff --git a/src/front/Components/DesignSystem/Table/SearchBar/index.tsx b/src/front/Components/DesignSystem/Table/SearchBar/index.tsx new file mode 100644 index 00000000..c407caf6 --- /dev/null +++ b/src/front/Components/DesignSystem/Table/SearchBar/index.tsx @@ -0,0 +1,33 @@ +import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import classes from "./classes.module.scss"; + +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { useDebounce } from "@uidotdev/usehooks"; + +type IProps = { + onChange?: (search: string) => void; + placeholder: string; +}; + +export default function SearchBar(props: IProps) { + const { placeholder, onChange } = props; + + const [search, setSearch] = useState(null); + const debouncedSearch = useDebounce(search, 200); + + const onSearch = useCallback((e: ChangeEvent) => { + setSearch(e.currentTarget.value); + }, []); + + useEffect(() => { + if (debouncedSearch === null) return; + onChange?.(debouncedSearch); + }, [debouncedSearch, onChange]); + + return ( +
+ + +
+ ); +} diff --git a/src/front/Components/DesignSystem/Table/classes.module.scss b/src/front/Components/DesignSystem/Table/classes.module.scss new file mode 100644 index 00000000..354de795 --- /dev/null +++ b/src/front/Components/DesignSystem/Table/classes.module.scss @@ -0,0 +1,13 @@ +.root { + .header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + + .input-container { + width: 300px; + cursor: text; + } + } +} diff --git a/src/front/Components/DesignSystem/Table/index.tsx b/src/front/Components/DesignSystem/Table/index.tsx new file mode 100644 index 00000000..1c4f2d15 --- /dev/null +++ b/src/front/Components/DesignSystem/Table/index.tsx @@ -0,0 +1,45 @@ +import classNames from "classnames"; +import MuiTable, { IRowProps, IHead } from "./MuiTable"; +import SearchBarTable from "./SearchBar"; +import classes from "./classes.module.scss"; +import { useCallback, useRef } from "react"; + +type IProps = { + header: readonly IHead[]; + rows: IRowProps[]; + count?: number; + className?: string; + onNext?: ((release: () => void, reset?: () => void) => Promise | void) | null; + searchBar?: { + placeholder?: string; + onSearch?: (search: string) => void; + }; +}; + +export default function Table(props: IProps) { + const { className, header, rows, searchBar } = props; + const keyId = useRef(0); + + const onSearch = useCallback( + (search: string) => { + keyId.current++; + searchBar?.onSearch?.(search); + }, + [searchBar], + ); + + return ( +
+ {searchBar && ( +
+
{props.count ?? rows.length} resultats
+
+ +
+
+ )} + + +
+ ); +} diff --git a/src/front/Components/DesignSystem/Tag/classes.module.scss b/src/front/Components/DesignSystem/Tag/classes.module.scss new file mode 100644 index 00000000..651f6877 --- /dev/null +++ b/src/front/Components/DesignSystem/Tag/classes.module.scss @@ -0,0 +1,27 @@ +@import "@Themes/constants.scss"; + +.root { + width: fit-content; + padding: 2px 8px; + border-radius: 360px; + + display: flex; + align-items: center; + justify-content: center; + + &.info { + background-color: var(--tag-info-background); + } + + &.success { + background-color: var(--tag-success-background); + } + + &.warning { + background-color: var(--tag-warning-background); + } + + &.error { + background-color: var(--tag-error-background); + } +} diff --git a/src/front/Components/DesignSystem/Tag/index.tsx b/src/front/Components/DesignSystem/Tag/index.tsx new file mode 100644 index 00000000..090a90fd --- /dev/null +++ b/src/front/Components/DesignSystem/Tag/index.tsx @@ -0,0 +1,48 @@ +import classNames from "classnames"; +import React from "react"; + +import Typography, { ETypo, ETypoColor } from "../Typography"; +import classes from "./classes.module.scss"; + +export enum ETagColor { + INFO = "info", + SUCCESS = "success", + ERROR = "error", + WARNING = "warning", +} + +export enum ETagVariant { + REGULAR = "regular", + SEMI_BOLD = "semi_bold", +} + +type IProps = { + label: string; + color: ETagColor; + variant?: ETagVariant; + className?: string; +}; + +const colorMap: Record = { + [ETagColor.INFO]: ETypoColor.COLOR_INFO_900, + [ETagColor.SUCCESS]: ETypoColor.COLOR_SUCCESS_700, + [ETagColor.ERROR]: ETypoColor.COLOR_SECONDARY_700, + [ETagColor.WARNING]: ETypoColor.COLOR_WARNING_700, +}; + +const typoMap: Record = { + [ETagVariant.REGULAR]: ETypo.TEXT_MD_REGULAR, + [ETagVariant.SEMI_BOLD]: ETypo.TEXT_XS_SEMIBOLD, +}; + +export default function Tag(props: IProps) { + const { className, label, color, variant = ETagVariant.REGULAR } = props; + + return ( +
+ + {label} + +
+ ); +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss new file mode 100644 index 00000000..3231922d --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss @@ -0,0 +1,23 @@ +.menu-item-wrapper { + width: 100%; + .menu-item { + display: flex; + padding: var(--spacing-md, 16px); + justify-content: flex-start; + align-items: center; + gap: var(--spacing-lg, 24px); + cursor: pointer; + + > svg { + width: 24px; + height: 24px; + transition: all ease-in-out 0.1s; + } + } + + .separator { + width: 100%; + height: 1px; + background-color: var(--separator-stroke-light, #d7dce0); + } +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx new file mode 100644 index 00000000..81071a54 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx @@ -0,0 +1,50 @@ +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) => { + 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 ( +
+
+ {React.cloneElement(element.icon, { color: `var(${getColor()})` })} + + {element.text} + +
+ {element.hasSeparator &&
} +
+ ); +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss b/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss new file mode 100644 index 00000000..28ddf217 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss @@ -0,0 +1,29 @@ +.root { + position: relative; + width: fit-content; + + .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); + 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; + &[data-opening-side="left"] { + left: auto; + right: 0px; + } + + &[data-opening-side="right"] { + left: 0px; + right: auto; + } + } +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/index.tsx b/src/front/Components/Elements/ButtonWithSubMenu/index.tsx new file mode 100644 index 00000000..1ef1a471 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/index.tsx @@ -0,0 +1,78 @@ +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(null); + const iconRef = useRef(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 ( +
+
+ +
+ + {isSubMenuOpened && ( +
+ {props.subElements.map((element, index) => { + return ; + })} +
+ )} +
+ ); +} diff --git a/src/front/Components/Elements/InfiniteScroll/index.tsx b/src/front/Components/Elements/InfiniteScroll/index.tsx new file mode 100644 index 00000000..15ee44a4 --- /dev/null +++ b/src/front/Components/Elements/InfiniteScroll/index.tsx @@ -0,0 +1,105 @@ +import React from "react"; + +/** + * @Documentation + * This component is used to create an infinite scroll effect. + * + * Usage: + * + * No pagination: front-end/src/components/pages/Products/index.tsx + + const onSearch = useCallback((searchParam: string) => productService.search(searchParam).then((products) => setRows(buildRows(products))), []); + + useEffect(() => { + productService.getLastProductSheets().then((products) => setRows(buildRows(products))); + }, []); + * + * + * + * With pagination: front-end/src/components/pages/Clients/index.tsx + * + const fetchClients = useCallback( + () => + clientService.get(pagination.current, search.current).then((clients) => { + if (clients.length === 0) return []; + setRows((_rows) => [..._rows, ...buildRows(clients)]); + pagination.current.skip += pagination.current.take; + return clients; + }), + [], + ); + + const onNext = useCallback( + (release: () => void) => { + fetchClients().then((clients) => { + if (!clients.length) return console.warn("No more value to load"); + release(); + }); + }, + [fetchClients], + ); + + const onSearch = useCallback((searchParam: string) => { + pagination.current.skip = 0; + search.current = (searchParam && searchParam.trim()) || null; + setRows([]); + }, []); + * + */ +export type IPagination = { + take: number; + skip: number; +}; + +type IProps = { + offset?: number; + orientation?: "vertical" | "horizontal"; + /** + * @description + * If `onNext` is set to `null`, it indicates that there is no pagination and the infinite scroll effect will not be triggered. + */ + onNext?: ((release: () => void, reset?: () => void) => Promise | void) | null; + children: React.ReactElement; +}; + +export default function InfiniteScroll({ children, onNext, offset = 20, orientation = "vertical" }: IProps) { + const isWaiting = React.useRef(false); + const elementRef = React.useRef(); + + const onChange = React.useCallback(() => { + if (!onNext) return; + const element = elementRef.current; + if (!element || isWaiting.current) return; + const { scrollTop, scrollLeft, clientHeight, clientWidth, scrollHeight, scrollWidth } = element; + let isChange = false; + + if (orientation === "vertical") isChange = scrollTop + clientHeight >= scrollHeight - offset; + if (orientation === "horizontal") isChange = scrollLeft + clientWidth >= scrollWidth - offset; + + if (isChange) { + isWaiting.current = true; + onNext(() => (isWaiting.current = false)); + } + }, [onNext, offset, orientation]); + + React.useEffect(() => onChange(), [onChange]); + + React.useEffect(() => { + const observer = new MutationObserver(onChange); + elementRef.current && observer.observe(elementRef.current, { childList: true, subtree: true }); + window.addEventListener("resize", onChange); + return () => { + observer.disconnect(); + window.removeEventListener("resize", onChange); + }; + }, [onChange]); + + if (!onNext) return children; + + const clonedChild = React.cloneElement(children, { + onScroll: onChange, + ref: elementRef, + }); + + return clonedChild; +} diff --git a/src/front/Components/Elements/Tabs/HorizontalTab/classes.module.scss b/src/front/Components/Elements/Tabs/HorizontalTab/classes.module.scss new file mode 100644 index 00000000..cfbbe33c --- /dev/null +++ b/src/front/Components/Elements/Tabs/HorizontalTab/classes.module.scss @@ -0,0 +1,12 @@ +.root { + padding: 8px 16px; + font-size: 16px; + letter-spacing: 0.08px; + + border-bottom: 1px solid var(--tabs-stroke, #d7dce0); + cursor: pointer; + + &[data-is-selected="true"] { + border-bottom: 2px solid var(--tabs-contrast-actived, #24282e); + } +} diff --git a/src/front/Components/Elements/Tabs/HorizontalTab/index.tsx b/src/front/Components/Elements/Tabs/HorizontalTab/index.tsx new file mode 100644 index 00000000..990e6238 --- /dev/null +++ b/src/front/Components/Elements/Tabs/HorizontalTab/index.tsx @@ -0,0 +1,34 @@ +import { useCallback } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import useHoverable from "@Front/Hooks/useHoverable"; +import { ITabValue } from ".."; +export type ITab = { + label: React.ReactNode; +}; + +export type IProps = { + onSelect: (value: ITabValue) => void; + value: ITabValue; + isSelected: boolean; +} & ITab; + +export default function HorizontalTabs(props: IProps) { + const onClick = useCallback(() => props.onSelect(props.value), [props]); + + const { isHovered, handleMouseEnter, handleMouseLeave } = useHoverable(); + return ( +
+ + {props.label} + +
+ ); +} diff --git a/src/front/Components/Elements/Tabs/VerticalTabs/classes.module.scss b/src/front/Components/Elements/Tabs/VerticalTabs/classes.module.scss new file mode 100644 index 00000000..abd080d5 --- /dev/null +++ b/src/front/Components/Elements/Tabs/VerticalTabs/classes.module.scss @@ -0,0 +1,4 @@ +.root { + padding: var(--spacing-md, 16px); + cursor: pointer; +} diff --git a/src/front/Components/Elements/Tabs/VerticalTabs/index.tsx b/src/front/Components/Elements/Tabs/VerticalTabs/index.tsx new file mode 100644 index 00000000..e54247bb --- /dev/null +++ b/src/front/Components/Elements/Tabs/VerticalTabs/index.tsx @@ -0,0 +1,24 @@ +import { useCallback } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import { ITabValue } from ".."; +export type ITab = { + label: React.ReactNode; +}; + +export type IProps = { + onSelect: (value: ITabValue) => void; + value: ITabValue; + isSelected: boolean; +} & ITab; + +export default function VerticalTabs(props: IProps) { + const onClick = useCallback(() => props.onSelect(props.value), [props]); + return ( +
+ + {props.label} + +
+ ); +} diff --git a/src/front/Components/Elements/Tabs/classes.module.scss b/src/front/Components/Elements/Tabs/classes.module.scss new file mode 100644 index 00000000..adfb7e37 --- /dev/null +++ b/src/front/Components/Elements/Tabs/classes.module.scss @@ -0,0 +1,50 @@ +.root { + .mirror-shadow-element { + display: flex; + overflow: hidden; + background: red; + height: 0px; + } + .horizontal-container { + display: flex; + flex-direction: row; + flex: 1; + .horizontal-tab { + display: flex; + justify-content: space-evenly; + overflow: hidden; + } + } + + .show-more-container { + position: relative; + border-bottom: 1px solid var(--color-neutral-500); + + .vertical-container { + position: absolute; + display: flex; + flex-direction: column; + top: 50px; + padding: var(--spacing-05, 4px) var(--spacing-2, 16px); + background: var(--color-generic-white, #fff); + + border: 1px solid var(--menu-border, #d7dce0); + border-radius: var(--menu-radius, 0px); + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1); + &[data-visible="false"] { + display: none; + } + } + } + + .show-more { + padding: 8px 16px; + display: flex; + color: white; + font-size: 16px; + justify-content: center; + align-items: center; + color: white; + cursor: pointer; + } +} diff --git a/src/front/Components/Elements/Tabs/index.tsx b/src/front/Components/Elements/Tabs/index.tsx new file mode 100644 index 00000000..7906b750 --- /dev/null +++ b/src/front/Components/Elements/Tabs/index.tsx @@ -0,0 +1,142 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import classes from "./classes.module.scss"; +import HorizontalTab, { ITab } from "./HorizontalTab"; +import VerticalTabs from "./VerticalTabs"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import { useWindowSize } from "@uidotdev/usehooks"; +import useOpenable from "@Front/Hooks/useOpenable"; + +export type ITabValue = T & { + id: unknown; +} + +type ITabInternal = ITab & { + key?: string; + value: ITabValue; +}; + +type IProps = { + tabs: ITabInternal[]; + onSelect: (value: T) => void; +}; + +export default function Tabs({ onSelect, tabs: propsTabs }: IProps) { + const tabs = useRef(propsTabs); + + const shadowElementRef = useRef(null); + const [visibleElements, setVisibleElements] = useState[]>([]); + const [overflowedElements, setOverflowedElements] = useState[]>([]); + + const [selectedTab, setSelectedTab] = useState>(tabs.current[0]!.value); + + const { close, isOpen, toggle } = useOpenable(); + + const windowSize = useWindowSize(); + + const calculateVisibleElements = useCallback(() => { + const shadowElement = shadowElementRef.current; + if (!shadowElement) return; + + const shadowElementWidth = shadowElement.offsetWidth; + // The first element is the show more element, it needs to be ignored in the calculation + let totalWidth = (shadowElement.children[0]! as HTMLElement).offsetWidth; + + let visibleCount = 0; + for (let i = 1; i < shadowElement.children.length; i++) { + totalWidth += (shadowElement.children[i]! as HTMLElement).offsetWidth; + if (totalWidth > shadowElementWidth) { + break; + } + visibleCount++; + } + + setVisibleElements(tabs.current.slice(0, visibleCount)); + setOverflowedElements(tabs.current.slice(visibleCount)); + }, []); + + useEffect(() => { + tabs.current = propsTabs; + }, [propsTabs]); + + useEffect(() => { + calculateVisibleElements(); + }, [calculateVisibleElements, windowSize]); + + const handleSelect = useCallback( + (value: ITabValue) => { + setSelectedTab(value); + onSelect(value); + close(); + calculateVisibleElements(); + }, + [close, onSelect, calculateVisibleElements], + ); + + const handleVerticalSelect = useCallback( + (value: ITabValue) => { + const index = tabs.current.findIndex((tab) => tab.value.id === value.id); + const newTabs = [...tabs.current]; + newTabs.splice(index, 1); + newTabs.unshift(tabs.current[index]!); + tabs.current = newTabs; + console.log("Updated values ; ", tabs.current); + handleSelect(value); + }, + [handleSelect], + ); + + return ( +
+
+
+ + {overflowedElements.length} de plus... + +
+ {tabs.current.map((element, index) => ( + + label={element.label} + key={element.key ?? index} + value={element.value} + onSelect={handleSelect} + isSelected={element.value.id === selectedTab.id} + /> + ))} +
+
+
+ {visibleElements.map((element, index) => ( + + label={element.label} + key={element.key ?? index} + value={element.value} + onSelect={handleSelect} + isSelected={element.value.id === selectedTab.id} + /> + ))} +
+ {overflowedElements.length > 0 && ( +
+
+ + {overflowedElements.length} de plus... + +
+
+ {overflowedElements.length > 0 && + overflowedElements.map((element, index) => ( + + label={element.label} + key={element.key ?? index} + value={element.value} + onSelect={handleVerticalSelect} + isSelected={selectedTab === element.value} + /> + ))} +
+
+ )} +
+
+ ); +} diff --git a/src/front/Components/Layouts/DesignSystem/classes.module.scss b/src/front/Components/Layouts/DesignSystem/classes.module.scss new file mode 100644 index 00000000..57f1d643 --- /dev/null +++ b/src/front/Components/Layouts/DesignSystem/classes.module.scss @@ -0,0 +1,24 @@ +.root { + display: flex; + flex-direction: column; + gap: 32px; + .components { + .inputs { + display: flex; + flex-direction: column; + gap: 24px; + } + display: flex; + flex-direction: column; + gap: 24px; + + .rows { + display: flex; + gap: 16px; + } + } + + .table { + max-width: 800px; + } +} diff --git a/src/front/Components/Layouts/DesignSystem/index.tsx b/src/front/Components/Layouts/DesignSystem/index.tsx new file mode 100644 index 00000000..4152498b --- /dev/null +++ b/src/front/Components/Layouts/DesignSystem/index.tsx @@ -0,0 +1,803 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Newsletter from "@Front/Components/DesignSystem/Newsletter"; +import Table from "@Front/Components/DesignSystem/Table"; +import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import NumberPicker from "@Front/Components/Elements/NumberPicker"; +import Tabs from "@Front/Components/Elements/Tabs"; +import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; +import useOpenable from "@Front/Hooks/useOpenable"; +import { + ArchiveBoxIcon, + ArrowLongLeftIcon, + ArrowLongRightIcon, + EllipsisHorizontalIcon, + PencilSquareIcon, + UsersIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { useCallback, useMemo, useState } from "react"; + +import classes from "./classes.module.scss"; +import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu"; + +export default function DesignSystem() { + const { isOpen, open, close } = useOpenable(); + const userDb = useMemo( + () => [ + { + username: "Maxime", + id: 1, + }, + { + username: "Vincent", + id: 2, + }, + { + username: "Massi", + id: 3, + }, + { + username: "Maxime", + id: 4, + }, + { + username: "Arnaud", + id: 5, + }, + ], + [], + ); + + const userDbArray = useMemo( + () => + userDb.map((user) => ({ + label: user.username, + key: user.id.toString(), + value: user, + })), + [userDb], + ); + + const [selectedTab, setSelectedTab] = useState<(typeof userDb)[number]>(userDb[0]!); + + const onSelect = useCallback((value: (typeof userDb)[number]) => { + setSelectedTab(value); + }, []); + + return ( + + DesignSystem + +
+
+ Button icon with menu + } + subElements={[ + { + icon: , + text: "Modifier les collaborateurs", + onClick: () => alert("yo"), + hasSeparator: true, + }, + { + icon: , + text: "Modifier les informations du dossier", + link: "/", + hasSeparator: true, + }, + { + icon: , + text: "Archiver le dossier", + link: "/", + color: ETypoColor.ERROR_WEAK_CONTRAST, + }, + ]} + /> + Inputs + Number picker avec min à 1 et max à 10 + {}} min={1} max={10} /> +
+ + + + + + + + Modal + + , rightIcon: }} + secondButton={{ children: "Confirmer", leftIcon: , rightIcon: }}> + Modal Content + + + Tabs + tabs={userDbArray} onSelect={onSelect} /> + + {selectedTab.id} - {selectedTab.username} + + + Circle Progress +
+ + + + +
+ + Tags +
+ + + + +
+ + Table Tags +
+ + + + +
+ + Table + + Send email + + ), + }, + { + key: "2", + name: "Doe", + firstname: "Jane", + button: , + }, + { + key: "3", + name: "Doe", + firstname: "Jack", + button: , + }, + ]} + /> + + Buttons +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ } variant={EIconButtonVariant.DEFAULT} /> + } variant={EIconButtonVariant.NEUTRAL} /> + } variant={EIconButtonVariant.PRIMARY} /> + } variant={EIconButtonVariant.ERROR} /> + } variant={EIconButtonVariant.SUCCESS} /> + } variant={EIconButtonVariant.WARNING} /> + } variant={EIconButtonVariant.INFO} /> + } variant={EIconButtonVariant.INFO} disabled /> +
+ + Alerts + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.INFO} + closeButton + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.ERROR} + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.WARNING} + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.SUCCESS} + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.NEUTRAL} + /> + + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx new file mode 100644 index 00000000..bdf407d9 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx @@ -0,0 +1,33 @@ +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import React, { useCallback } from "react"; + +type IProps = { + customerUid: string; + isOpen: boolean; + onClose?: () => void; + + onDelete: (customerUid: string) => void; +}; + +export default function DeleteCustomerModal(props: IProps) { + const { isOpen, onClose, onDelete } = props; + + const handleDelete = useCallback(() => { + onDelete(props.customerUid); + }, [onDelete, props.customerUid]); + + return ( + + + Cette action retirera le client de ce dossier. Vous ne pourrez plus récupérer les informations associées à ce client dans ce + dossier une fois supprimées. + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/classes.module.scss new file mode 100644 index 00000000..5e75f6fa --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/classes.module.scss @@ -0,0 +1,22 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + width: fit-content; + padding: var(--spacing-md, 16px); + flex-direction: column; + gap: var(--spacing-md, 16px); + background: var(--primary-weak-higlight, #e5eefa); + min-width: 300px; + + .header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + + .delete-button { + margin: auto; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx new file mode 100644 index 00000000..b667ad9c --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx @@ -0,0 +1,141 @@ +import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import useOpenable from "@Front/Hooks/useOpenable"; +import { PencilSquareIcon, TrashIcon, UsersIcon } from "@heroicons/react/24/outline"; + +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; + anchorStatus: AnchorStatus; + folderUid: string | undefined; + customerNote: Note | null; + onDelete: (customerUid: string) => void; +}; + +export default function ClientBox(props: IProps) { + const { customer, anchorStatus, folderUid, customerNote } = props; + + const { isOpen: isDeleteModalOpen, open: openDeleteModal, close: closeDeleteModal } = useOpenable(); + const { isOpen: isErrorModalOpen, open: openErrorModal, close: closeErrorModal } = useOpenable(); + + const handleDelete = useCallback( + (customerUid: string) => { + if (customer.documents && customer.documents.length > 0) { + closeDeleteModal(); + openErrorModal(); + return; + } + props.onDelete(customerUid); + }, + [closeDeleteModal, customer.documents, openErrorModal, props], + ); + + let createOrUpdateNotePath = Module.getInstance() + .get() + .modules.pages.Notes.pages.EditNote.props.path.replace("[noteUid]", customerNote?.uid ?? ""); + if (!customerNote) { + createOrUpdateNotePath = Module.getInstance() + .get() + .modules.pages.Notes.pages.CreateNote.props.path.replace("[folderUid]", folderUid ?? ""); + createOrUpdateNotePath = createOrUpdateNotePath.replace("[customerUid]", customer.uid ?? ""); + } + + return ( +
+
+ + {customer.contact?.first_name} {customer.contact?.last_name} + + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + } + openingSide="right" + subElements={[ + { + icon: , + text: "Modifier les informations", + link: Module.getInstance() + .get() + .modules.pages.Folder.pages.EditClient.props.path.replace("[folderUid]", folderUid ?? "") + .replace("[customerUid]", customer.uid ?? ""), + hasSeparator: true, + }, + { + icon: , + text: "Modifier la note", + link: createOrUpdateNotePath, + }, + ]} + /> + // + // + // + )} +
+
+ + Numéro de téléphone + + + {customer.contact?.cell_phone_number ?? customer.contact?.phone_number ?? "_"} + +
+
+ + E-mail + + + {customer.contact?.email ?? "_"} + +
+
+ + Note client + + + {customerNote?.content ?? "-"} + +
+ + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + <> + + + + )} + + + Ce client ne peut pas être supprimé car des documents sont associés à son dossier. Veuillez d'abord gérer ou supprimer + ces documents avant de tenter de supprimer le client. + + +
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx new file mode 100644 index 00000000..901780d9 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx @@ -0,0 +1,37 @@ +import Documents from "@Front/Api/LeCoffreApi/Notary/Documents/Documents"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import React, { useCallback } from "react"; + +type IProps = { + documentUid: string; + isOpen: boolean; + onClose?: () => void; + + onDeleteSuccess: (uid: string) => void; +}; + +export default function DeleteAskedDocumentModal(props: IProps) { + const { isOpen, onClose, documentUid, onDeleteSuccess } = props; + + const onDelete = useCallback( + () => + Documents.getInstance() + .delete(documentUid) + .then(() => onDeleteSuccess(documentUid)) + .then(onClose) + .catch((error) => console.warn(error)), + [documentUid, onClose, onDeleteSuccess], + ); + + return ( + + Cette action annulera la demande du document en cours. + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx new file mode 100644 index 00000000..6ef96822 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx @@ -0,0 +1,20 @@ +import Modal from "@Front/Components/DesignSystem/Modal"; +import { File } from "le-coffre-resources/dist/Customer"; +import React from "react"; + +type IProps = { + file: File; + url: string; + isOpen: boolean; + onClose?: () => void; +}; + +export default function FilePreviewModal(props: IProps) { + const { isOpen, onClose, file, url } = props; + + return ( + + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/classes.module.scss new file mode 100644 index 00000000..1e85e972 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/classes.module.scss @@ -0,0 +1,14 @@ +@import "@Themes/constants.scss"; + +.root { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); + + .title { + display: flex; + justify-content: space-between; + align-items: center; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx new file mode 100644 index 00000000..8d85e9f2 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx @@ -0,0 +1,226 @@ +import Files from "@Front/Api/LeCoffreApi/Notary/Files/Files"; +import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; +import IconButton from "@Front/Components/DesignSystem/IconButton"; +import Table from "@Front/Components/DesignSystem/Table"; +import { IHead, IRowProps } from "@Front/Components/DesignSystem/Table/MuiTable"; +import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import Module from "@Front/Config/Module"; +import useOpenable from "@Front/Hooks/useOpenable"; +import { ArrowDownTrayIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { Document } from "le-coffre-resources/dist/Customer"; +import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document"; +import Link from "next/link"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +import classes from "./classes.module.scss"; +import DeleteAskedDocumentModal from "./DeleteAskedDocumentModal"; + +type IProps = { + documents: Document[]; + folderUid: string; +}; + +const header: readonly IHead[] = [ + { + key: "document_type", + title: "Type de document", + }, + { + key: "document_status", + title: "Statut", + }, + { + key: "created_at", + title: "Demandé le", + }, + { + key: "actions", + title: "Actions", + }, +]; + +const tradDocumentStatus: Record = { + [EDocumentStatus.ASKED]: "Demandé", + [EDocumentStatus.DEPOSITED]: "À valider", + [EDocumentStatus.VALIDATED]: "Validé", + [EDocumentStatus.REFUSED]: "Refusé", +}; + +export default function DocumentTables(props: IProps) { + const { documents: documentsProps, folderUid } = props; + const [documents, setDocuments] = useState(documentsProps); + const [documentUid, setDocumentUid] = useState(null); + + const deleteAskedOocumentModal = useOpenable(); + + useEffect(() => { + setDocuments(documentsProps); + }, [documentsProps]); + + const openDeleteAskedDocumentModal = useCallback( + (uid: string | undefined) => { + if (!uid) return; + setDocumentUid(uid); + deleteAskedOocumentModal.open(); + }, + [deleteAskedOocumentModal], + ); + + const onDownload = useCallback((doc: Document) => { + const file = doc.files?.[0]; + if (!file || !file?.uid) return; + + return Files.getInstance() + .download(file.uid) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = file.file_name ?? "file"; + a.click(); + URL.revokeObjectURL(url); + }) + .catch((e) => console.warn(e)); + }, []); + + const askedDocuments: IRowProps[] = useMemo( + () => + documents + .map((document) => { + if (document.document_status !== EDocumentStatus.ASKED) return null; + return { + key: document.uid, + document_type: document.document_type?.name ?? "_", + document_status: ( + + ), + created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_", + actions: openDeleteAskedDocumentModal(document.uid)} />} />, + }; + }) + .filter((document) => document !== null) as IRowProps[], + [documents, openDeleteAskedDocumentModal], + ); + + const toValidateDocuments: IRowProps[] = useMemo( + () => + documents + .map((document) => { + if (document.document_status !== EDocumentStatus.DEPOSITED) return null; + return { + key: document.uid, + document_type: document.document_type?.name ?? "_", + document_status: ( + + ), + created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_", + actions: ( + + } /> + + ), + }; + }) + .filter((document) => document !== null) as IRowProps[], + [documents, folderUid], + ); + + const validatedDocuments: IRowProps[] = useMemo( + () => + documents + .map((document) => { + if (document.document_status !== EDocumentStatus.VALIDATED) return null; + return { + key: document.uid, + document_type: document.document_type?.name ?? "_", + document_status: ( + + ), + created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_", + actions: ( +
+ + } /> + + onDownload(document)} icon={} /> +
+ ), + }; + }) + .filter((document) => document !== null) as IRowProps[], + [documents, folderUid, onDownload], + ); + + const refusedDocuments: IRowProps[] = useMemo( + () => + documents + .map((document) => { + if (document.document_status !== EDocumentStatus.REFUSED) return null; + return { + key: document.uid, + document_type: document.document_type?.name ?? "_", + document_status: ( + + ), + created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_", + actions: "", + }; + }) + .filter((document) => document !== null) as IRowProps[], + [documents], + ); + + const progress = useMemo(() => { + const total = askedDocuments.length + toValidateDocuments.length + validatedDocuments.length + refusedDocuments.length; + if (total === 0) return 0; + return (validatedDocuments.length / total) * 100; + }, [askedDocuments.length, refusedDocuments.length, toValidateDocuments.length, validatedDocuments.length]); + + return ( +
+
+ + Documents + + +
+ {askedDocuments.length > 0 &&
} + {toValidateDocuments.length > 0 &&
} + {validatedDocuments.length > 0 &&
} + {refusedDocuments.length > 0 &&
} + {documentUid && ( + setDocuments(documents.filter((document) => document.uid !== uid))} + documentUid={documentUid} + /> + )} + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/classes.module.scss new file mode 100644 index 00000000..0594b1b7 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/classes.module.scss @@ -0,0 +1,7 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/index.tsx new file mode 100644 index 00000000..49de66dc --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/NoDocument/index.tsx @@ -0,0 +1,20 @@ +import EmptyAlert from "@Front/Components/DesignSystem/EmptyAlert"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import { DocumentIcon } from "@heroicons/react/24/outline"; + +import classes from "./classes.module.scss"; + +export default function NoDocument() { + return ( +
+ + Documents + + } + title="Aucune demande de document" + description="Vous n'avez encore demandé aucun document pour ce client. Pour commencer, cliquez sur le bouton ci-dessous pour créer une nouvelle demande de document." + /> +
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/classes.module.scss new file mode 100644 index 00000000..673d2297 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/classes.module.scss @@ -0,0 +1,30 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); + + .tab-container { + display: flex; + gap: var(--spacing-md, 16px); + justify-content: space-between; + align-items: center; + + .tabs { + width: calc(100% - 210px); + } + } + + .content { + display: flex; + gap: var(--spacing-lg, 24px); + + .client-box { + display: flex; + flex-direction: column; + + gap: var(--spacing-lg, 24px); + } + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx new file mode 100644 index 00000000..cce182f9 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx @@ -0,0 +1,115 @@ +import Tabs from "@Front/Components/Elements/Tabs"; +import Customer from "le-coffre-resources/dist/Customer"; +import { OfficeFolder } from "le-coffre-resources/dist/Notary"; + +import { useCallback, useMemo, useState } from "react"; + +import { AnchorStatus } from ".."; +import classes from "./classes.module.scss"; +import ClientBox from "./ClientBox"; +import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { DocumentIcon, UserPlusIcon } from "@heroicons/react/24/outline"; +import Module from "@Front/Config/Module"; +import Link from "next/link"; +import NoDocument from "./NoDocument"; +import DocumentTables from "./DocumentTables"; +import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; + +type IProps = { + folder: OfficeFolder; + anchorStatus: AnchorStatus; +}; + +export type ICustomer = Customer & { id: string }; + +export default function ClientView(props: IProps) { + const { folder, anchorStatus } = props; + + const customers: ICustomer[] = useMemo( + () => + folder?.customers?.map((customer) => ({ + id: customer.uid ?? "", + ...customer, + })) ?? [], + [folder], + ); + + const [customer, setCustomer] = useState<(typeof customers)[number]>(customers[0]!); + + const tabs = useMemo( + () => + customers.map((customer) => ({ + label: `${customer.contact?.first_name} ${customer.contact?.last_name}`, + key: customer.uid, + value: customer, + })), + [customers], + ); + + const doesCustomerHaveDocument = useMemo(() => customer.documents && customer.documents.length > 0, [customer]); + + const handleClientDelete = useCallback( + (customerUid: string) => { + if (!folder.uid) return; + Folders.getInstance().put( + folder.uid, + OfficeFolder.hydrate({ + ...folder, + customers: folder.customers?.filter((customer) => customer.uid !== customerUid), + }), + ); + window.location.reload(); + }, + [folder], + ); + + return ( +
+
+
{tabs && tabs={tabs} onSelect={setCustomer} />}
+ + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + + + + )} +
+
+
+ value.customer?.uid === customer.uid) ?? null} + /> + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + + + + )} +
+ {doesCustomerHaveDocument ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/classes.module.scss new file mode 100644 index 00000000..dd4839c0 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/classes.module.scss @@ -0,0 +1,50 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + gap: var(--spacing-lg, 40px); + .info-box1 { + display: flex; + width: 100%; + flex-direction: column; + gap: var(--spacing-sm, 8px); + + .open-date { + display: flex; + gap: var(--spacing-sm, 8px); + } + } + + .info-box2 { + display: flex; + flex-direction: column; + gap: var(--spacing-lg, 24px); + width: 100%; + max-width: 400px; + + .progress-container { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + .icon-container { + display: flex; + gap: var(--spacing-md, 8px); + } + } + + .description-container { + .text { + max-height: 60px; + overflow-y: auto; + } + } + } + + .separator { + background-color: var(--separator-stroke-light); + width: 1px; + align-self: stretch; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx new file mode 100644 index 00000000..4e60f216 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx @@ -0,0 +1,103 @@ +import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; +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"; + +type IProps = { + folder: OfficeFolder | null; + progress: number; + onArchive: () => void; + anchorStatus: AnchorStatus; + isArchived: boolean; +}; + +export default function InformationSection(props: IProps) { + const { folder, progress, onArchive, anchorStatus, isArchived } = props; + + const getSubMenuElement = useCallback(() => { + let elements: ISubElement[] = []; + + // Creating the three elements and adding them conditionnally + const modifyCollaboratorsElement = { + icon: , + text: "Modifier les collaborateurs", + link: Module.getInstance() + .get() + .modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", folder?.uid ?? ""), + hasSeparator: true, + }; + const modifyInformationsElement = { + icon: , + text: "Modifier les informations du dossier", + link: Module.getInstance() + .get() + .modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""), + hasSeparator: true, + }; + + const archiveElement = { + icon: , + text: "Archiver le dossier", + onClick: onArchive, + color: ETypoColor.ERROR_WEAK_CONTRAST, + }; + + // If the folder is not anchored, we can modify the collaborators and the informations + if (anchorStatus === AnchorStatus.NOT_ANCHORED) { + elements.push(modifyCollaboratorsElement); + // Remove the separator if it's the last item (if the folder is not archived) + if (isArchived) modifyInformationsElement.hasSeparator = false; + + elements.push(modifyInformationsElement); + } + + // If the folder is not archived, we can archive it + if (!isArchived) { + elements.push(archiveElement); + } + return elements; + }, [anchorStatus, folder?.uid, isArchived, onArchive]); + return ( +
+
+
+ {folder?.folder_number} + {folder?.name} +
+ + +
+ Ouverture du dossier + + {folder?.created_at ? new Date(folder.created_at).toLocaleDateString() : ""} + +
+
+
+ +
+
+ +
+ } subElements={getSubMenuElement()} /> +
+
+
+ + Note du dossier + + + {folder?.description} + +
+
+
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/AddClientSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/AddClientSection/index.tsx new file mode 100644 index 00000000..09421703 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/AddClientSection/index.tsx @@ -0,0 +1,34 @@ +import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import EmptyAlert from "@Front/Components/DesignSystem/EmptyAlert"; +import Module from "@Front/Config/Module"; +import { UserPlusIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; +import React, { useMemo } from "react"; + +type IProps = { + folderUid: string; +}; + +export default function AddClientSection(props: IProps) { + const { folderUid } = props; + + const addClientPath = useMemo(() => { + if (!folderUid) return ""; + return Module.getInstance().get().modules.pages.Folder.pages.AddClient.props.path.replace("[folderUid]", folderUid); + }, [folderUid]); + + return ( + } + title="Ajouter des clients au dossier" + description="Pour pouvoir faire une demande de document, vous devez d'abord ajouter un ou plusieurs clients à ce dossier. Cette étape est essentielle pour assurer le suivi et la gestion des documents." + footer={ + + + + } + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx new file mode 100644 index 00000000..814f5d56 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx @@ -0,0 +1,42 @@ +import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Module from "@Front/Config/Module"; +import { OfficeFolder } from "le-coffre-resources/dist/Notary"; +import { useRouter } from "next/router"; +import React, { useCallback } from "react"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folder: OfficeFolder; +}; + +export default function DeleteFolderModal(props: IProps) { + const { isOpen, onClose, folder } = props; + const router = useRouter(); + + const onDelete = useCallback(() => { + if (!folder.uid) return; + if ((folder?.customers?.length ?? 0) > 0 || (folder?.documents?.length ?? 0) > 0) + return console.warn("Cannot delete folder with customers or documents"); + + return Folders.getInstance() + .delete(folder.uid) + .then(() => router.push(Module.getInstance().get().modules.pages.Folder.props.path)) + .then(onClose); + }, [folder, router, onClose]); + + return ( + + + Cette action est irréversible. En supprimant ce dossier, toutes les informations associées seront définitivement perdues. + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/classes.module.scss new file mode 100644 index 00000000..9c8a3edd --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/classes.module.scss @@ -0,0 +1,11 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); + + .delete-button { + align-self: flex-end; + } +} \ No newline at end of file diff --git a/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/index.tsx new file mode 100644 index 00000000..3eb99a8d --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/index.tsx @@ -0,0 +1,42 @@ +import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { OfficeFolder } from "le-coffre-resources/dist/Notary"; +import { useMemo } from "react"; + +import AddClientSection from "./AddClientSection"; +import classes from "./classes.module.scss"; +import DeleteFolderModal from "./DeleteFolderModal"; +import useOpenable from "@Front/Hooks/useOpenable"; +import { AnchorStatus } from ".."; + +type IProps = { + folder: OfficeFolder; + anchorStatus: AnchorStatus; +}; + +export default function NoClientView(props: IProps) { + const { folder, anchorStatus } = props; + + const deleteFolderModal = useOpenable(); + const canDeleteFolder = useMemo(() => folder.documents?.length === 0 && folder.customers?.length === 0, [folder]); + + return ( +
+ {anchorStatus === AnchorStatus.NOT_ANCHORED && } + {canDeleteFolder && ( + <> + + + + )} +
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx new file mode 100644 index 00000000..bbdf1378 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx @@ -0,0 +1,25 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import { EButtonstyletype } from "@Front/Components/DesignSystem/Button"; +import { LockClosedIcon } from "@heroicons/react/24/outline"; + +type IProps = { + onAnchor: () => void; +}; + +export default function AnchoringAlertInfo(props: IProps) { + const { onAnchor } = props; + return ( + , + onClick: onAnchor, + }} + variant={EAlertVariant.INFO} + fullWidth + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx new file mode 100644 index 00000000..0c5ae96f --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx @@ -0,0 +1,36 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import { EButtonstyletype } from "@Front/Components/DesignSystem/Button"; +import { ArrowDownOnSquareIcon, CheckIcon } from "@heroicons/react/24/outline"; + +type IProps = { + onDownloadAnchoringProof: () => void; + onArchive: () => void; + isArchived: boolean; +}; + +export default function AnchoringAlertSuccess(props: IProps) { + const { onDownloadAnchoringProof, onArchive, isArchived } = props; + return ( + , + onClick: onDownloadAnchoringProof, + }} + secondButton={ + isArchived + ? undefined + : { + children: "Archiver le dossier", + onClick: onArchive, + } + } + variant={EAlertVariant.SUCCESS} + icon={} + fullWidth + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss new file mode 100644 index 00000000..f0139309 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss @@ -0,0 +1,11 @@ +.anchoring { + display: flex; + flex-direction: column; + gap: 24px; + + .validate-gif { + width: 100%; + height: 100%; + object-fit: contain; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx new file mode 100644 index 00000000..071ddc34 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx @@ -0,0 +1,62 @@ +import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors"; +import ValidateAnchoringGif from "@Front/Assets/images/validate_anchoring.gif"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Image from "next/image"; +import React, { useCallback, useState } from "react"; + +import classes from "./classes.module.scss"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folderUid: string; + onAnchorSuccess: () => void; +}; + +export default function AnchoringModal(props: IProps) { + const { isOpen, onClose, folderUid, onAnchorSuccess } = props; + const [isAnchoring, setIsAnchoring] = useState(false); + + const anchor = useCallback(() => { + const timeoutDelay = 9800; + const timeoutPromise = new Promise((resolve) => { + setTimeout(resolve, timeoutDelay); + }); + setIsAnchoring(true); + return OfficeFolderAnchors.getInstance() + .post(folderUid) + .then(() => timeoutPromise) + .then(() => setIsAnchoring(false)) + .then(onAnchorSuccess) + .then(onClose) + .catch((e) => { + console.warn(e); + setIsAnchoring(false); + }); + }, [folderUid, onAnchorSuccess, onClose]); + + return ( + + {!isAnchoring ? ( + + La certification et l'ancrage de ce dossier dans la blockchain sont des actions définitives et garantiront la sécurité + et l'authenticité de tous les documents. Veuillez confirmer que vous souhaitez continuer. + + ) : ( +
+ + Vos documents sont en train d'être ancrés dans la blockchain. Cela peut prendre quelques instants. Merci de votre + patience. + + +
+ )} +
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringProcessingInfo/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringProcessingInfo/index.tsx new file mode 100644 index 00000000..599d4173 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringProcessingInfo/index.tsx @@ -0,0 +1,14 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import { ArrowPathIcon } from "@heroicons/react/24/outline"; + +export default function AnchoringProcessingInfo() { + return ( + } + fullWidth + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveAlertWarning/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveAlertWarning/index.tsx new file mode 100644 index 00000000..89828c08 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveAlertWarning/index.tsx @@ -0,0 +1,48 @@ +import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import { EButtonstyletype } from "@Front/Components/DesignSystem/Button"; +import Module from "@Front/Config/Module"; +import { ArchiveBoxArrowDownIcon, ArchiveBoxIcon, ArrowDownOnSquareIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/router"; +import { useCallback } from "react"; + +type IProps = { + onDownloadAnchoringProof: () => void; + folderUid: string; +}; + +export default function ArchiveAlertWarning(props: IProps) { + const { onDownloadAnchoringProof, folderUid } = props; + + const router = useRouter(); + + const restoreArchive = useCallback(() => { + Folders.getInstance() + .restore(folderUid) + .then(() => router.push(Module.getInstance().get().modules.pages.Folder.props.path)) + .catch((e) => { + console.warn(e); + }); + }, [folderUid, router]); + + return ( + , + onClick: onDownloadAnchoringProof, + }} + secondButton={{ + children: "Restaurer le dossier", + onClick: restoreArchive, + rightIcon: , + }} + variant={EAlertVariant.WARNING} + icon={} + fullWidth + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss new file mode 100644 index 00000000..455f1de2 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss @@ -0,0 +1,5 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx new file mode 100644 index 00000000..177ddf8e --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx @@ -0,0 +1,48 @@ +import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; +import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Module from "@Front/Config/Module"; +import { useRouter } from "next/router"; +import React, { useCallback } from "react"; +import classes from "./classes.module.scss"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folderUid: string; +}; + +export default function ArchiveModal(props: IProps) { + const { isOpen, onClose, folderUid } = props; + const router = useRouter(); + + const archive = useCallback(() => { + const description = (document.querySelector("textarea[name='archived_description']") as HTMLTextAreaElement).value ?? ""; + + Folders.getInstance() + .archive(folderUid, description) + .then(onClose) + .then(() => router.push(Module.getInstance().get().modules.pages.Folder.props.path)) + .catch((e) => { + console.warn(e); + }); + }, [folderUid, onClose, router]); + + return ( + +
+ + Archiver ce dossier le déplacera dans la section des dossiers archivés. Vous pouvez ajouter une note de dossier avant + d'archiver si vous le souhaitez. + + +
+
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx new file mode 100644 index 00000000..910076a4 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx @@ -0,0 +1,47 @@ +import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import { OfficeFolder } from "le-coffre-resources/dist/Notary"; +import React, { useCallback } from "react"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folder: OfficeFolder; +}; + +export default function DownloadAnchoringProofModal(props: IProps) { + const { isOpen, onClose, folder } = props; + + const downloadAnchoringProof = useCallback(async () => { + if (!folder?.uid) return; + try { + const file = await OfficeFolderAnchors.getInstance().download(folder.uid); + const url = window.URL.createObjectURL(file); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = `anchoring_proof_${folder?.folder_number}_${folder?.name}.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + onClose?.(); + } catch (e) { + console.warn(e); + } + }, [folder?.folder_number, folder?.name, folder.uid, onClose]); + + return ( + + + Votre dossier a été validé et ancré dans la blockchain. Vous pouvez maintenant télécharger la preuve d'ancrage pour vos + archives. + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss new file mode 100644 index 00000000..f0139309 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss @@ -0,0 +1,11 @@ +.anchoring { + display: flex; + flex-direction: column; + gap: 24px; + + .validate-gif { + width: 100%; + height: 100%; + object-fit: contain; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx new file mode 100644 index 00000000..d6273927 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx @@ -0,0 +1,32 @@ +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import React, { useCallback } from "react"; + +type IProps = { + isOpen: boolean; + onClose: () => void; + onAnchor: () => void; +}; + +export default function RequireAnchoringModal(props: IProps) { + const { isOpen, onClose, onAnchor: onAnchorProps } = props; + + const onAnchor = useCallback(() => { + onAnchorProps(); + onClose(); + }, [onAnchorProps, onClose]); + + return ( + + + Pour archiver ce dossier, il est nécessaire de l'ancrer dans la blockchain afin de garantir la sécurité et l'authenticité + des documents. Veuillez procéder à l'ancrage avant de continuer. + + + ); +} diff --git a/src/front/Components/Layouts/Login/PasswordForgotten/classes.module.scss b/src/front/Components/Layouts/Login/PasswordForgotten/classes.module.scss new file mode 100644 index 00000000..e0a34824 --- /dev/null +++ b/src/front/Components/Layouts/Login/PasswordForgotten/classes.module.scss @@ -0,0 +1,29 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .form { + margin-top: 32px; + + .password_indication { + margin-top: 8px; + margin-bottom: 24px; + } + .submit_button { + margin-top: 32px; + } + } +} diff --git a/src/front/Components/Layouts/Login/PasswordForgotten/index.tsx b/src/front/Components/Layouts/Login/PasswordForgotten/index.tsx new file mode 100644 index 00000000..074ee9ee --- /dev/null +++ b/src/front/Components/Layouts/Login/PasswordForgotten/index.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { ValidationError } from "class-validator"; +type IProps = { + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; +}; + +export default function PasswordForgotten(props: IProps) { + const { onSubmit, validationErrors } = props; + + return ( +
+ +
Réinitialisez votre mot de passe
+
+
+ error.property === "password")} + password + /> + + Au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre. + + error.property === "confirm_password")} + password + /> + + +
+ ); +} diff --git a/src/front/Components/Layouts/Login/StepEmail/classes.module.scss b/src/front/Components/Layouts/Login/StepEmail/classes.module.scss new file mode 100644 index 00000000..2826c0af --- /dev/null +++ b/src/front/Components/Layouts/Login/StepEmail/classes.module.scss @@ -0,0 +1,32 @@ +.root { + width: 472px; + margin: auto; + margin-top: 80px; + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); + + .header { + display: flex; + flex-direction: column; + gap: var(--spacing-sm, 8px); + } + + .content { + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); + + .section { + .section-title { + margin-bottom: var(--spacing-xl, 32px); + } + + > form { + display: flex; + flex-direction: column; + gap: var(--spacing-md, 16px); + } + } + } +} diff --git a/src/front/Components/Layouts/Login/StepEmail/index.tsx b/src/front/Components/Layouts/Login/StepEmail/index.tsx new file mode 100644 index 00000000..68d351d2 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepEmail/index.tsx @@ -0,0 +1,194 @@ +import React, { useCallback, useEffect, useState } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +//import Image from "next/image"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button from "@Front/Components/DesignSystem/Button"; +//import franceConnectLogo from "../france-connect.svg"; +// import { useRouter } from "next/router"; +// import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; +import { ValidationError } from "class-validator"; +import Image from "next/image"; +import LogoSmallBlue from "@Assets/logo_small_blue.svg"; + +import idNoteLogo from "@Assets/Icons/id-note-logo.svg"; +import { useRouter } from "next/router"; +import { FrontendVariables } from "@Front/Config/VariablesFront"; +import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm"; + +type IProps = { + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; +}; + +export default function StepEmail(props: IProps) { + const { onSubmit, validationErrors } = props; + const [isErrorModalOpen, setIsErrorModalOpen] = useState(0); + /* const router = useRouter(); + const redirectCustomerOnConnection = useCallback(() => { + async function getCustomer() { + try { + const loginRes = await Customers.getInstance().login(); + router.push(loginRes.enrollment.franceConnectUrl); + } catch (e) { + console.error(e); + } + } + getCustomer(); + }, [router]); */ + + const router = useRouter(); + const error = router.query["error"]; + const redirectUserOnConnection = useCallback(() => { + const variables = FrontendVariables.getInstance(); + router.push( + `${variables.IDNOT_BASE_URL + variables.IDNOT_AUTHORIZE_ENDPOINT}?client_id=${variables.IDNOT_CLIENT_ID}&redirect_uri=${ + variables.FRONT_APP_HOST + }/authorized-client&scope=openid,profile&response_type=code`, + ); + }, [router]); + + const openErrorModal = useCallback((index: number) => { + setIsErrorModalOpen(index); + }, []); + + const closeErrorModal = useCallback(() => { + setIsErrorModalOpen(0); + }, []); + + const closeNoEmailModal = useCallback(() => { + setIsErrorModalOpen(0); + router.push("https://connexion.idnot.fr/"); + }, [router]); + + const closeContactAdminModal = () => { + setIsErrorModalOpen(0); + window.open("https://www.lecoffre.io/contact", "_blank"); + }; + + useEffect(() => { + openErrorModal(parseInt(error as string)); + }, [error, openErrorModal]); + + return ( +
+
+ + + Bienvenue ! + + Connectez-vous pour accéder à votre espace sécurisé. +
+
+
+ + Pour les notaires : + + +
+
+ + Pour les clients : + +
+ err.property === "email")} + /> + + +
+
+ +
+ + Vous ne disposez pas d'un abonnement, veuillez contacter l'administrateur de votre office. + +
+
+ +
+ + Veuillez vous reconnecter. + +
+
+ +
+ + Votre compte ID.not doit être associé à une adresse email @notaires.fr (onglet Mettre à jour mes données + professionnelles) + +
+
+ +
+ + L'accès à la version bêta de lecoffre.io est limité à un groupe restreint d'utilisateurs autorisés. + +
    +
  • + + Si vous êtes intéressé par la participation à notre programme de bêta-test, veuillez nous compléter le + formulaire :{" "} + + https://www.lecoffre.io/contact + + +
  • +
    +
  • + + Si vous avez déjà un compte bêta-testeur, veuillez vous connecter sur{" "} + + https://compte.idnot.fr/home + {" "} + et vérifier que l'adresse mail renseignée sur votre espace est identique à celle que vous nous avez + communiquée. + +
  • +
+
+
+
+ ); +} diff --git a/src/front/Components/Layouts/Login/StepNewPassword/classes.module.scss b/src/front/Components/Layouts/Login/StepNewPassword/classes.module.scss new file mode 100644 index 00000000..e0a34824 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepNewPassword/classes.module.scss @@ -0,0 +1,29 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .form { + margin-top: 32px; + + .password_indication { + margin-top: 8px; + margin-bottom: 24px; + } + .submit_button { + margin-top: 32px; + } + } +} diff --git a/src/front/Components/Layouts/Login/StepNewPassword/index.tsx b/src/front/Components/Layouts/Login/StepNewPassword/index.tsx new file mode 100644 index 00000000..94ba48c3 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepNewPassword/index.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { ValidationError } from "class-validator"; +type IProps = { + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; +}; + +export default function StepNewPassword(props: IProps) { + const { onSubmit, validationErrors } = props; + + return ( +
+ +
Configurez votre mot de passe
+
+
+ error.property === "password")} + password + /> + + Au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre. + + error.property === "confirm_password")} + password + /> + + +
+ ); +} diff --git a/src/front/Components/Layouts/Login/StepPassword/classes.module.scss b/src/front/Components/Layouts/Login/StepPassword/classes.module.scss new file mode 100644 index 00000000..0244c442 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepPassword/classes.module.scss @@ -0,0 +1,30 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .form { + margin-top: 32px; + .submit_button { + margin-top: 32px; + } + + .forgot-password { + margin-top: 8px; + text-decoration: underline; + cursor: pointer; + } + } +} diff --git a/src/front/Components/Layouts/Login/StepPassword/index.tsx b/src/front/Components/Layouts/Login/StepPassword/index.tsx new file mode 100644 index 00000000..f05511a9 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepPassword/index.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { ValidationError } from "class-validator"; +import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm"; +type IProps = { + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; + onPasswordForgotClicked: () => void; +}; + +export default function StepPassword(props: IProps) { + const { onSubmit, validationErrors, onPasswordForgotClicked } = props; + const [isModalOpened, setIsModalOpened] = React.useState(false); + + const closeModal = () => { + setIsModalOpened(false); + }; + + const openModal = () => { + setIsModalOpened(true); + }; + + const onModalAccept = () => { + onPasswordForgotClicked(); + setIsModalOpened(false); + }; + + return ( +
+ +
Entrez votre mot de passe
+
+
+ error.property === "password")} + password + /> +
+ + Mot de passe oublié ? + +
+ + + +
+ + Un code à usage unique va vous être envoyé par sms pour réinitialiser votre mot de passe. + +
+
+
+ ); +} diff --git a/src/front/Components/Layouts/Login/StepTotp/classes.module.scss b/src/front/Components/Layouts/Login/StepTotp/classes.module.scss new file mode 100644 index 00000000..fabaa162 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepTotp/classes.module.scss @@ -0,0 +1,43 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: 220px auto; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .form { + margin-top: 32px; + .submit_button { + margin-top: 32px; + } + } + + .ask-another-code { + margin-top: 48px; + display: flex; + flex-direction: column; + gap: 16px; + align-items: flex-start; + + .new-code-button { + &[data-disabled="true"] { + opacity: 0.5; + cursor: not-allowed; + } + } + .new-code-timer { + display: flex; + gap: 6px; + align-items: center; + } + } +} diff --git a/src/front/Components/Layouts/Login/StepTotp/index.tsx b/src/front/Components/Layouts/Login/StepTotp/index.tsx new file mode 100644 index 00000000..d954ef82 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepTotp/index.tsx @@ -0,0 +1,77 @@ +import React, { useEffect } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { ValidationError } from "class-validator"; +type IProps = { + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; + partialPhoneNumber: string; + onSendAnotherCode: () => void; +}; + +export default function StepTotp(props: IProps) { + const { onSubmit, validationErrors, partialPhoneNumber, onSendAnotherCode } = props; + const [disableNewCodeButton, setDisableNewCodeButton] = React.useState(false); + const [secondsBeforeNewCode, setSecondsBeforeNewCode] = React.useState(0); + + useEffect(() => { + const interval = setInterval(() => { + if (secondsBeforeNewCode > 0) { + setSecondsBeforeNewCode(secondsBeforeNewCode - 1); + if (secondsBeforeNewCode === 1) { + setDisableNewCodeButton(false); + } + } + }, 1000); + return () => clearInterval(interval); + }, [secondsBeforeNewCode]); + + const sendAnotherCode = () => { + onSendAnotherCode(); + setDisableNewCodeButton(true); + setSecondsBeforeNewCode(30); + }; + + return ( +
+ +
+ Votre code a été envoyé par SMS au ** ** ** {partialPhoneNumber.replace(/(.{2})/g, "$1 ")} +
+
+
+ error.property === "totpCode")} + /> + + +
+ Vous n'avez rien reçu ? + + {secondsBeforeNewCode !== 0 && ( + + Redemandez un code dans + + 00:{secondsBeforeNewCode < 10 ? `0${secondsBeforeNewCode}` : secondsBeforeNewCode} + + + )} +
+
+ ); +} diff --git a/src/front/Components/Layouts/Login/france-connect.svg b/src/front/Components/Layouts/Login/france-connect.svg new file mode 100644 index 00000000..98ae5ac4 --- /dev/null +++ b/src/front/Components/Layouts/Login/france-connect.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/front/Themes/constants-old.scss b/src/front/Themes/constants-old.scss new file mode 100644 index 00000000..975d1a60 --- /dev/null +++ b/src/front/Themes/constants-old.scss @@ -0,0 +1,44 @@ +// $screen-xl: 2559px; +$screen-l: 1439px; +$screen-ls: 1280px; +$screen-m: 1023px; +$screen-s: 767px; +// $screen-xs: 424px; + +$custom-easing: cubic-bezier(0.645, 0.045, 0.355, 1); + +// Generic colors +$black: #000000; +$white: #ffffff; + +// Flash colors +$green-flash: #12bf4d; +$blue-flash: #005176; +$turquoise-flash: #3fa79e; +$purple-flash: #320756; +$purple-hover: #4e1480; +$orange-flash: #ffb017; +$red-flash: #a63a23; +$re-hover: #cc4c31; +$pink-flash: #bd4b91; +$pink-hover: #e34ba9; + +// Soft colors +$green-soft: #baf2cd; +$blue-soft: #a7c6d4; +$orange-soft: #ffdc99; +$turquoise-soft: #c3eae6; +$purple-soft: #c5b2d4; +$orange-soft: #ffdc99; +$red-soft: #f08771; +$pink-soft: #f8b9df; + +$orange-soft-hover: #ffd078; +$grey: #939393; +$grey-medium: #e7e7e7; +$grey-soft: #f9f9f9; + +$modal-background: rgba(0, 0, 0, 0.44); + +$shadow-nav: 0px 8px 10px rgba(0, 0, 0, 0.07); +$shadow-tooltip: 0px 4px 24px rgba(0, 0, 0, 0.15); diff --git a/src/front/Themes/variables-old.scss b/src/front/Themes/variables-old.scss new file mode 100644 index 00000000..31b5f274 --- /dev/null +++ b/src/front/Themes/variables-old.scss @@ -0,0 +1,36 @@ +@import "@Themes/constants.scss"; + +:root { + --root-max-width: 1440px; + --root-margin: auto; + --root-padding: 64px 120px; + + --font-text-family: "Inter", sans-serif; + + --green-flash: #{$green-flash}; + --blue-flash: #{$blue-flash}; + --turquoise-flash: #{$turquoise-flash}; + --purple-flash: #{$purple-flash}; + --purple-hover: #{$purple-hover}; + --orange-flash: #{$orange-flash}; + --red-flash: #{$red-flash}; + --re-hover: #{$re-hover}; + --pink-flash: #{$pink-flash}; + --pink-hover: #{$pink-hover}; + + --green-soft: #{$green-soft}; + --blue-soft: #{$blue-soft}; + --turquoise-soft: #{$turquoise-soft}; + --purple-soft: #{$purple-soft}; + --orange-soft: #{$orange-soft}; + --orange-soft-hover: #{$orange-soft-hover}; + --red-soft: #{$red-soft}; + --pink-soft: #{$pink-soft}; + + --grey: #{$grey}; + --grey-medium: #{$grey-medium}; + --grey-soft: #{$grey-soft}; + + --black: #{$black}; + --white: #{$white}; +} diff --git a/src/pages/design-system/index.tsx b/src/pages/design-system/index.tsx new file mode 100644 index 00000000..d3835570 --- /dev/null +++ b/src/pages/design-system/index.tsx @@ -0,0 +1,5 @@ +import DesignSystem from "@Front/Components/Layouts/DesignSystem"; + +export default function Route() { + return ; +}