diff --git a/public/favicon.svg b/public/favicon.svg index 984d1ebe..74592a78 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,22 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 3b435604..7b9ece0e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,15 +1,15 @@ { - "short_name": "lecoffre", - "name": "lecoffre", - "icons": [ - { - "src": "/favicon.ico", - "sizes": "32x32 16x16", - "type": "image/x-icon" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "light", - "background_color": "light" + "short_name": "lecoffre", + "name": "lecoffre", + "icons": [ + { + "src": "/favicon.svg", + "sizes": "32x32 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "light", + "background_color": "light" } diff --git a/src/front/Components/DesignSystem/Autocomplete/classes.module.scss b/src/front/Components/DesignSystem/Autocomplete/classes.module.scss new file mode 100644 index 00000000..0a331a1d --- /dev/null +++ b/src/front/Components/DesignSystem/Autocomplete/classes.module.scss @@ -0,0 +1,7 @@ +@import "@Themes/constants.scss"; + +.root { + .label { + padding: 0px var(--spacing-2, 16px); + } +} diff --git a/src/front/Components/DesignSystem/Autocomplete/index.tsx b/src/front/Components/DesignSystem/Autocomplete/index.tsx new file mode 100644 index 00000000..8ecfc3db --- /dev/null +++ b/src/front/Components/DesignSystem/Autocomplete/index.tsx @@ -0,0 +1,82 @@ +import useOpenable from "@Front/Hooks/useOpenable"; +import { useCallback, useEffect, useState } from "react"; + +import DropdownMenu from "../Dropdown/DropdownMenu"; +import { IOption } from "../Dropdown/DropdownMenu/DropdownOption"; +import SearchBar from "../SearchBar"; +import Typography, { ETypo, ETypoColor } from "../Typography"; +import classes from "./classes.module.scss"; + +type IProps = { + options: IOption[]; + placeholder?: string; + disabled?: boolean; + label?: string; + onSelect?: (option: IOption) => void; + selectedOption?: IOption | null; +}; + +export default function Autocomplete(props: IProps) { + const { options, placeholder, disabled, label, selectedOption: selectedOptionProps } = props; + const [selectedOption, setSelectedOption] = useState(selectedOptionProps ?? null); + const [searchValue, setSearchValue] = useState(""); + const [filteredOptions, setFilteredOptions] = useState(options); + const openable = useOpenable({ defaultOpen: false }); + + useEffect(() => { + if (searchValue) { + const filteredOptions = options.filter((option) => getLabel(option)?.toLowerCase().includes(searchValue.toLowerCase())); + console.log(filteredOptions); + if (filteredOptions.length === 0) + return setFilteredOptions([{ id: "no-results", label: "Aucun résulats", notSelectable: true }]); + return setFilteredOptions(filteredOptions); + } + return setFilteredOptions(options); + }, [searchValue, options]); + + const handleSearchChange = useCallback( + (value: string) => { + setSearchValue(value); + if (value) { + openable.open(); + } else { + openable.close(); + } + }, + [openable], + ); + + useEffect(() => { + setSelectedOption(selectedOptionProps ?? null); + }, [selectedOptionProps]); + + const handleSelectOption = useCallback( + (option: IOption) => { + setSelectedOption(option); + setSearchValue(getLabel(option) || ""); + openable.close(); + }, + [openable], + ); + + function getLabel(option: IOption | null): string | null { + if (!option) return null; + if (typeof option.label === "string") { + return option.label; + } + return `${option.label.text} ${option.label.subtext}`; + } + + return ( + +
+ {label && ( + + {label} + + )} +
+ +
+ ); +} diff --git a/src/front/Components/DesignSystem/CheckBox/index.tsx b/src/front/Components/DesignSystem/CheckBox/index.tsx index 6b697557..7f467391 100644 --- a/src/front/Components/DesignSystem/CheckBox/index.tsx +++ b/src/front/Components/DesignSystem/CheckBox/index.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { IOption } from "../Form/SelectField"; import Tooltip from "../ToolTip"; import Typography, { ETypo, ETypoColor } from "../Typography"; import classes from "./classes.module.scss"; import classNames from "classnames"; +import { IOptionOld } from "../Form/SelectFieldOld"; type IProps = { name?: string; - option: IOption; + option: IOptionOld; toolTip?: string; onChange?: (e: React.ChangeEvent) => void; checked: boolean; diff --git a/src/front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption/classes.module.scss b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption/classes.module.scss new file mode 100644 index 00000000..0cd1dc23 --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption/classes.module.scss @@ -0,0 +1,38 @@ +.root { + display: flex; + padding: var(--spacing-1, 8px) var(--spacing-2, 16px); + align-items: center; + gap: var(--spacing-sm, 8px); + justify-content: space-between; + + border-radius: var(--dropdown-radius, 0px); + border: 1px solid var(--dropdown-border, rgba(0, 0, 0, 0)); + + background: var(--dropdown-option-background-default, #fff); + + svg { + width: 24px; + height: 24px; + } + + .content { + display: flex; + flex-direction: column; + align-items: flex-start; + flex: 1 0 0; + } + + &:hover { + background-color: var(--dropdown-option-background-hovered); + } + + &:focus, + &:active { + background-color: var(--dropdown-option-background-pressed); + } + + &[data-not-selectable="true"] { + pointer-events: none; + user-select: none; + } +} diff --git a/src/front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption/index.tsx b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption/index.tsx new file mode 100644 index 00000000..dee89d43 --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption/index.tsx @@ -0,0 +1,56 @@ +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import { CheckIcon } from "@heroicons/react/24/outline"; +import { useCallback } from "react"; + +import classes from "./classes.module.scss"; + +export type IOption = { + id: string; + label: string | { text: string; subtext: string }; + notSelectable?: boolean; +}; + +type IProps = { + option: IOption; + isActive: boolean; + onClick?: (option: IOption) => void; +}; + +export default function DropdownOption(props: IProps) { + const { option, onClick, isActive } = props; + + const handleOnClick = useCallback(() => onClick && onClick(option), [onClick, option]); + + return ( +
+ {getContent(option.label)} + {isActive && } +
+ ); + + function getContent(label: string | { text: string; subtext: string }) { + if (typeof label === "string") { + return ( + + {label} + + ); + } + return ( +
+ + {label.text} + + + {label.subtext} + +
+ ); + } +} diff --git a/src/front/Components/DesignSystem/Dropdown/DropdownMenu/classes.module.scss b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/classes.module.scss new file mode 100644 index 00000000..d8742a45 --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/classes.module.scss @@ -0,0 +1,36 @@ +.root { + position: relative; + overflow: hidden; + + .content { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 3; + + padding: var(--spacing-sm, 8px); + border-radius: var(--dropdown-radius, 0px); + + background: var(--dropdown-menu-background, #fff); + + border: 1px solid var(--dropdown-menu-border-primary, #005bcb); + + max-height: 0; + opacity: 0; + transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out; + + position: absolute; + top: 100%; + left: 0; + transform: translateY(8px); + } + + &.open { + overflow: visible; + .content { + max-height: 500px; + opacity: 1; + } + } +} diff --git a/src/front/Components/DesignSystem/Dropdown/DropdownMenu/index.tsx b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/index.tsx new file mode 100644 index 00000000..c19f6982 --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/index.tsx @@ -0,0 +1,44 @@ +import classNames from "classnames"; +import React, { useCallback } from "react"; + +import classes from "./classes.module.scss"; +import DropdownOption, { IOption } from "./DropdownOption"; + +type IProps = { + options: IOption[]; + selectedOption: IOption | null; + children: React.ReactNode; + openable: { + isOpen: boolean; + open: () => void; + close: () => void; + toggle: () => void; + }; + onSelect?: (option: IOption) => void; +}; +export default function DropdownMenu(props: IProps) { + const { children, options, onSelect, openable, selectedOption } = props; + + const handleSelect = useCallback( + (option: IOption) => { + onSelect?.(option); + openable.close(); + }, + [onSelect, openable], + ); + + return ( +
+ {children} +
+ {options.map((option) => { + return ; + })} +
+
+ ); + + function isActive(option: IOption): boolean { + return selectedOption?.id === option.id; + } +} diff --git a/src/front/Components/DesignSystem/Dropdown/classes.module.scss b/src/front/Components/DesignSystem/Dropdown/classes.module.scss new file mode 100644 index 00000000..ccb9c08a --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/classes.module.scss @@ -0,0 +1,64 @@ +@import "@Themes/constants.scss"; + +.root { + .label { + padding: 0px var(--spacing-2, 16px); + } + .container { + cursor: pointer; + + display: flex; + align-items: center; + height: 56px; + + padding: var(--spacing-2, 16px) var(--spacing-sm, 8px); + gap: var(--spacing-2, 16px); + + border-radius: var(--input-radius, 0px); + border: 1px solid var(--dropdown-input-border-default, #d7dce0); + background: var(--dropdown-input-background, #fff); + + .content { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + height: 24px; + .value { + width: 100%; + display: flex; + padding: 0px var(--spacing-2, 16px); + align-items: center; + flex: 1 0 0; + } + + svg { + width: 24px; + height: 24px; + cursor: pointer; + stroke: var(--button-icon-button-default-default); + } + } + + &:hover { + border-color: var(--dropdown-input-border-hovered); + } + + &.active { + border-color: var(--dropdown-input-border-filled); + } + + &.open { + border-color: var(--dropdown-input-border-expanded); + + svg { + transform: rotate(180deg); + } + } + + &.disabled { + opacity: var(--opacity-disabled, 0.3); + pointer-events: none; + } + } +} diff --git a/src/front/Components/DesignSystem/Dropdown/index.tsx b/src/front/Components/DesignSystem/Dropdown/index.tsx new file mode 100644 index 00000000..a3875994 --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/index.tsx @@ -0,0 +1,74 @@ +import useOpenable from "@Front/Hooks/useOpenable"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import classNames from "classnames"; +import { useCallback, useEffect, useState } from "react"; + +import Typography, { ETypo, ETypoColor } from "../Typography"; +import classes from "./classes.module.scss"; +import DropdownMenu from "./DropdownMenu"; +import { IOption } from "./DropdownMenu/DropdownOption"; + +type IProps = { + options: IOption[]; + label?: string; + placeholder?: string; + disabled?: boolean; + onSelect?: (option: IOption) => void; + selectedOption?: IOption | null; +}; + +export default function Dropdown(props: IProps) { + const { options, placeholder, disabled, onSelect, selectedOption: selectedOptionProps, label } = props; + const [selectedOption, setSelectedOption] = useState(selectedOptionProps ?? null); + const openable = useOpenable({ defaultOpen: false }); + + useEffect(() => { + setSelectedOption(selectedOptionProps ?? null); + }, [selectedOptionProps]); + + const handleOnSelect = useCallback( + (option: IOption) => { + setSelectedOption(option); + onSelect?.(option); + }, + [onSelect], + ); + + return ( + +
+ {label && ( + + {label} + + )} +
+
+ + {getLabel(selectedOption) ?? placeholder} + + +
+
+
+
+ ); +} + +export function getLabel(option: IOption | null): string | null { + if (!option) return null; + if (typeof option.label === "string") { + return option.label; + } + return `${option.label.text} ${option.label.subtext}`; +} diff --git a/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss b/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss index deb6e813..560b12a5 100644 --- a/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss +++ b/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss @@ -1,23 +1,7 @@ @import "@Themes/constants.scss"; .root { - width: calc(100vh - 83px); display: flex; flex-direction: column; justify-content: space-between; - - .header { - flex: 1; - } - - .searchbar { - padding: 40px 24px 24px 24px; - } - - .folderlist-container { - max-height: calc(100vh - 290px); - height: calc(100vh - 290px); - overflow: auto; - border-right: 1px solid var(--color-neutral-200); - } } diff --git a/src/front/Components/DesignSystem/Form/AutocompleteField/classes.module.scss b/src/front/Components/DesignSystem/Form/AutocompleteField/classes.module.scss new file mode 100644 index 00000000..caa72fd2 --- /dev/null +++ b/src/front/Components/DesignSystem/Form/AutocompleteField/classes.module.scss @@ -0,0 +1,11 @@ +@import "@Themes/constants.scss"; + +.root { + .hidden-input { + position: absolute; + opacity: 0; + } + .errors-container { + margin-top: 8px; + } +} diff --git a/src/front/Components/DesignSystem/Form/AutocompleteField/index.tsx b/src/front/Components/DesignSystem/Form/AutocompleteField/index.tsx new file mode 100644 index 00000000..ce36524a --- /dev/null +++ b/src/front/Components/DesignSystem/Form/AutocompleteField/index.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { ReactNode } from "react"; + +import Autocomplete from "../../Autocomplete"; +import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption"; +import BaseField, { IProps as IBaseFieldProps, IState as IBaseFieldState } from "../BaseField"; +import classes from "./classes.module.scss"; + +export type IProps = IBaseFieldProps & { + onSelect?: (option: IOption) => void; + options: IOption[]; + selectedOption?: IOption | null; + label?: string; +}; + +type IState = IBaseFieldState & { + selectedOption: IOption | null; +}; + +export default class AutocompleteField extends BaseField { + constructor(props: IProps) { + super(props); + this.state = { + selectedOption: this.props.selectedOption ?? null, + ...this.getDefaultState(), + }; + + this.handleOnChange = this.handleOnChange.bind(this); + } + + private handleOnChange = (option: IOption) => { + this.setState({ selectedOption: option }); + this.props.onSelect?.(option); + }; + + public override componentDidUpdate(prevProps: IProps): void { + if (prevProps.selectedOption !== this.props.selectedOption) { + this.setState({ selectedOption: this.props.selectedOption ?? null }); + } + } + + public override render(): ReactNode { + return ( +
+ + {this.state.selectedOption && ( + + )} + {this.hasError() &&
{this.renderErrors()}
} +
+ ); + } +} diff --git a/src/front/Components/DesignSystem/Form/BaseField.tsx b/src/front/Components/DesignSystem/Form/BaseField.tsx index 3fe14388..fd16a00a 100644 --- a/src/front/Components/DesignSystem/Form/BaseField.tsx +++ b/src/front/Components/DesignSystem/Form/BaseField.tsx @@ -18,7 +18,7 @@ export type IProps = { label?: string; }; -type IState = { +export type IState = { value: string; validationError: ValidationError | null; }; diff --git a/src/front/Components/DesignSystem/Form/SelectField/classes.module.scss b/src/front/Components/DesignSystem/Form/SelectField/classes.module.scss index 07a3b243..caa72fd2 100644 --- a/src/front/Components/DesignSystem/Form/SelectField/classes.module.scss +++ b/src/front/Components/DesignSystem/Form/SelectField/classes.module.scss @@ -1,143 +1,11 @@ @import "@Themes/constants.scss"; .root { - display: flex; - position: relative; - flex-direction: column; - width: 100%; - border: 1px solid var(--color-neutral-200); - - &[data-errored="true"] { - border: 1px solid var(--color-error-600); + .hidden-input { + position: absolute; + opacity: 0; } - - &[data-disabled="true"] { - .container-label { - cursor: not-allowed; - } - opacity: 0.6; - } - - .container-label { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - background-color: var(--color-generic-white); - cursor: pointer; - padding: 24px; - z-index: 1; - - &[data-border-right-collapsed="true"] { - border-radius: 8px 0 0 8px; - } - - .container-input chevron-icon { - display: flex; - align-items: center; - - span { - display: flex; - - .icon { - display: flex; - margin-right: 8px; - align-items: center; - } - } - - .placeholder { - position: absolute; - top: 24px; - left: 8px; - background-color: var(--color-generic-white); - padding: 0 16px; - - &[data-open="true"] { - transform: translateY(-36px); - } - } - } - - .chevron-icon { - height: 24px; - - fill: var(--color-neutral-500); - transition: all 350ms $custom-easing; - transform: rotate(90deg); - - &[data-open="true"] { - transform: rotate(-90deg); - } - } - } - - .container-ul { - padding-left: 24px; - z-index: 3; - list-style: none; - margin: 0; - outline: 0; - display: flex; - flex-direction: column; - width: 100%; - transition: height 350ms $custom-easing, opacity 350ms $custom-easing; - opacity: 1; - overflow: hidden; - top: 50px; - background-color: var(--color-generic-white); - - &[data-open="false"] { - height: 0; - opacity: 0; - border: none; - } - } - - .container-li { - display: flex; - justify-content: flex-start; - align-items: center; - padding-bottom: 24px; - border-radius: 8px; - cursor: pointer; - background: var(--color-neutral-50); - - &:hover { - background: var(--color-neutral-100); - } - - &:active { - background: var(--color-neutral-200); - } - - span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .token-icon { - max-width: 20px; - display: flex; - align-items: center; - margin-right: 11px; - - > svg { - height: 20px; - margin-right: 11px; - } - - > img { - height: 20px; - width: 20px; - } - } - - .backdrop { - position: fixed; - z-index: 1; - inset: 0; + .errors-container { + margin-top: 8px; } } diff --git a/src/front/Components/DesignSystem/Form/SelectField/index.tsx b/src/front/Components/DesignSystem/Form/SelectField/index.tsx index 55418357..3a79d80c 100644 --- a/src/front/Components/DesignSystem/Form/SelectField/index.tsx +++ b/src/front/Components/DesignSystem/Form/SelectField/index.tsx @@ -1,212 +1,65 @@ -import ChevronIcon from "@Assets/Icons/chevron.svg"; -import WindowStore from "@Front/Stores/WindowStore"; -import { ValidationError } from "class-validator"; -import classNames from "classnames"; -import Image from "next/image"; -import React, { FormEvent, ReactNode } from "react"; +import React from "react"; +import { ReactNode } from "react"; -import Typography, { ETypo, ETypoColor } from "../../Typography"; +import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption"; +import BaseField, { IProps as IBaseFieldProps, IState as IBaseFieldState } from "../BaseField"; import classes from "./classes.module.scss"; -import { NextRouter, useRouter } from "next/router"; +import Dropdown from "../../Dropdown"; -type IProps = { - selectedOption?: IOption; - onChange?: (selectedOption: IOption) => void; +export type IProps = IBaseFieldProps & { + onSelect?: (option: IOption) => void; options: IOption[]; - hasBorderRightCollapsed?: boolean; - placeholder?: string; - className?: string; - name: string; - disabled?: boolean; - errors?: ValidationError; + selectedOption?: IOption | null; + label?: string; }; -export type IOption = { - value: unknown; - label: string; - icon?: ReactNode; - description?: string; -}; - -type IState = { - isOpen: boolean; - listWidth: number; - listHeight: number; +type IState = IBaseFieldState & { selectedOption: IOption | null; - errors: ValidationError | null; }; -type IPropsClass = IProps & { - router: NextRouter; -}; - -class SelectFieldClass extends React.Component { - private contentRef = React.createRef(); - private rootRef = React.createRef(); - private removeOnresize = () => {}; - - static defaultProps = { - disabled: false, - }; - - constructor(props: IPropsClass) { +export default class SelectField extends BaseField { + constructor(props: IProps) { super(props); this.state = { - isOpen: false, - listHeight: 0, - listWidth: 0, - selectedOption: null, - errors: this.props.errors ?? null, + selectedOption: this.props.selectedOption ?? null, + ...this.getDefaultState(), }; - this.toggle = this.toggle.bind(this); - this.onSelect = this.onSelect.bind(this); + + this.handleOnChange = this.handleOnChange.bind(this); } - public override render(): JSX.Element { - const selectedOption = this.state.selectedOption ?? this.props.selectedOption; + private handleOnChange = (option: IOption) => { + this.setState({ selectedOption: option }); + this.props.onSelect?.(option); + }; + + public override componentDidUpdate(prevProps: IProps): void { + if (prevProps.selectedOption !== this.props.selectedOption) { + this.setState({ selectedOption: this.props.selectedOption ?? null }); + } + } + + public override render(): ReactNode { return ( -
-
- {selectedOption && } - - -
    - {this.props.options.map((option, index) => ( -
  • this.onSelect(option, e)}> -
    {option.icon}
    - {option.label} -
  • - ))} -
- - {this.state.isOpen &&
} -
- {this.state.errors !== null &&
{this.renderErrors()}
} +
+ + {this.state.selectedOption && ( + + )} + {this.hasError() &&
{this.renderErrors()}
}
); } - public override componentDidMount(): void { - this.onResize(); - this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize()); - - this.props.router.events.on("routeChangeStart", () => { - this.setState({ - isOpen: false, - selectedOption: null, - listHeight: 0, - listWidth: 0, - }); - }); - } - - public override componentWillUnmount() { - this.removeOnresize(); - } - - public override componentDidUpdate(prevProps: IProps) { - if (this.props.errors !== prevProps.errors) { - this.setState({ - errors: this.props.errors ?? null, - }); - } - - if (this.props.selectedOption !== prevProps.selectedOption) { - this.setState({ - selectedOption: this.props.selectedOption ?? null, - }); - } - } - - static getDerivedStateFromProps(props: IProps, state: IState) { - if (props.selectedOption?.value) { - return { - value: props.selectedOption?.value, - }; - } - return null; - } - - private onResize() { - let listHeight = 0; - let listWidth = 0; - listWidth = this.rootRef.current?.scrollWidth ?? 0; - if (this.state.listHeight) { - listHeight = this.contentRef.current?.scrollHeight ?? 0; - } - this.setState({ listHeight, listWidth }); - } - - private toggle(e: FormEvent) { - if (this.props.disabled) return; - e.preventDefault(); - let listHeight = 0; - let listWidth = this.rootRef.current?.scrollWidth ?? 0; - - if (!this.state.listHeight) { - listHeight = this.contentRef.current?.scrollHeight ?? 0; - } - - this.setState((state) => { - return { isOpen: !state.isOpen, listHeight, listWidth }; - }); - } - - private onSelect(option: IOption, e: React.MouseEvent) { - if (this.props.disabled) return; - this.props.onChange && this.props.onChange(option); - this.setState({ - selectedOption: option, - }); - this.toggle(e); - } - - private renderErrors(): JSX.Element | null { - if (!this.state.errors) return null; - return ( - - {this.props.placeholder} ne peut pas être vide - - ); - } -} - -export default function SelectField(props: IProps) { - const router = useRouter(); - return ; } diff --git a/src/front/Components/DesignSystem/Form/SelectFieldOld/classes.module.scss b/src/front/Components/DesignSystem/Form/SelectFieldOld/classes.module.scss new file mode 100644 index 00000000..caa72fd2 --- /dev/null +++ b/src/front/Components/DesignSystem/Form/SelectFieldOld/classes.module.scss @@ -0,0 +1,11 @@ +@import "@Themes/constants.scss"; + +.root { + .hidden-input { + position: absolute; + opacity: 0; + } + .errors-container { + margin-top: 8px; + } +} diff --git a/src/front/Components/DesignSystem/Form/SelectFieldOld/index.tsx b/src/front/Components/DesignSystem/Form/SelectFieldOld/index.tsx new file mode 100644 index 00000000..ba077302 --- /dev/null +++ b/src/front/Components/DesignSystem/Form/SelectFieldOld/index.tsx @@ -0,0 +1,212 @@ +import ChevronIcon from "@Assets/Icons/chevron.svg"; +import WindowStore from "@Front/Stores/WindowStore"; +import { ValidationError } from "class-validator"; +import classNames from "classnames"; +import Image from "next/image"; +import React, { FormEvent, ReactNode } from "react"; + +import Typography, { ETypo, ETypoColor } from "../../Typography"; +import classes from "./classes.module.scss"; +import { NextRouter, useRouter } from "next/router"; + +type IProps = { + selectedOption?: IOptionOld; + onChange?: (selectedOption: IOptionOld) => void; + options: IOptionOld[]; + hasBorderRightCollapsed?: boolean; + placeholder?: string; + className?: string; + name: string; + disabled?: boolean; + errors?: ValidationError; +}; + +export type IOptionOld = { + value: unknown; + label: string; + icon?: ReactNode; + description?: string; +}; + +type IState = { + isOpen: boolean; + listWidth: number; + listHeight: number; + selectedOption: IOptionOld | null; + errors: ValidationError | null; +}; + +type IPropsClass = IProps & { + router: NextRouter; +}; + +class SelectFieldClass extends React.Component { + private contentRef = React.createRef(); + private rootRef = React.createRef(); + private removeOnresize = () => {}; + + static defaultProps = { + disabled: false, + }; + + constructor(props: IPropsClass) { + super(props); + this.state = { + isOpen: false, + listHeight: 0, + listWidth: 0, + selectedOption: null, + errors: this.props.errors ?? null, + }; + this.toggle = this.toggle.bind(this); + this.onSelect = this.onSelect.bind(this); + } + + public override render(): JSX.Element { + const selectedOption = this.state.selectedOption ?? this.props.selectedOption; + return ( +
+
+ {selectedOption && } + + +
    + {this.props.options.map((option, index) => ( +
  • this.onSelect(option, e)}> +
    {option.icon}
    + {option.label} +
  • + ))} +
+ + {this.state.isOpen &&
} +
+ {this.state.errors !== null &&
{this.renderErrors()}
} +
+ ); + } + public override componentDidMount(): void { + this.onResize(); + this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize()); + + this.props.router.events.on("routeChangeStart", () => { + this.setState({ + isOpen: false, + selectedOption: null, + listHeight: 0, + listWidth: 0, + }); + }); + } + + public override componentWillUnmount() { + this.removeOnresize(); + } + + public override componentDidUpdate(prevProps: IProps) { + if (this.props.errors !== prevProps.errors) { + this.setState({ + errors: this.props.errors ?? null, + }); + } + + if (this.props.selectedOption !== prevProps.selectedOption) { + this.setState({ + selectedOption: this.props.selectedOption ?? null, + }); + } + } + + static getDerivedStateFromProps(props: IProps, state: IState) { + if (props.selectedOption?.value) { + return { + value: props.selectedOption?.value, + }; + } + return null; + } + + private onResize() { + let listHeight = 0; + let listWidth = 0; + listWidth = this.rootRef.current?.scrollWidth ?? 0; + if (this.state.listHeight) { + listHeight = this.contentRef.current?.scrollHeight ?? 0; + } + this.setState({ listHeight, listWidth }); + } + + private toggle(e: FormEvent) { + if (this.props.disabled) return; + e.preventDefault(); + let listHeight = 0; + let listWidth = this.rootRef.current?.scrollWidth ?? 0; + + if (!this.state.listHeight) { + listHeight = this.contentRef.current?.scrollHeight ?? 0; + } + + this.setState((state) => { + return { isOpen: !state.isOpen, listHeight, listWidth }; + }); + } + + private onSelect(option: IOptionOld, e: React.MouseEvent) { + if (this.props.disabled) return; + this.props.onChange && this.props.onChange(option); + this.setState({ + selectedOption: option, + }); + this.toggle(e); + } + + private renderErrors(): JSX.Element | null { + if (!this.state.errors) return null; + return ( + + {this.props.placeholder} ne peut pas être vide + + ); + } +} + +export default function SelectField(props: IProps) { + const router = useRouter(); + return ; +} diff --git a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/BurgerModalSubmenu/classes.module.scss b/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/BurgerModalSubmenu/classes.module.scss deleted file mode 100644 index 26b47c0f..00000000 --- a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/BurgerModalSubmenu/classes.module.scss +++ /dev/null @@ -1,18 +0,0 @@ -@import "@Themes/constants.scss"; - -.root { - .content { - display: flex; - gap: 8px; - align-items: center; - justify-content: center; - } - - .sub-menu { - padding: 24px; - text-align: center; - gap: 24px; - display: flex; - flex-direction: column; - } -} diff --git a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/BurgerModalSubmenu/index.tsx b/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/BurgerModalSubmenu/index.tsx deleted file mode 100644 index 7b1adba8..00000000 --- a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/BurgerModalSubmenu/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import classNames from "classnames"; -import { useRouter } from "next/router"; -import React, { useEffect, useState } from "react"; -import classes from "./classes.module.scss"; -import { IAppRule } from "@Front/Api/Entities/rule"; -import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; -import { IHeaderLinkProps } from "../../../ButtonHeader"; -import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; -import HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink"; -import useToggle from "@Front/Hooks/useToggle"; -import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; - -type IProps = { - text: string | JSX.Element; - links: (IHeaderLinkProps & { - rules?: IAppRule[]; - })[]; -}; - -export default function HeaderSubmenu(props: IProps) { - const router = useRouter(); - const { pathname } = router; - const [isActive, setIsActive] = useState(true); - const { active: isSubmenuOpened, toggle } = useToggle(); - - useEffect(() => { - setIsActive(false); - if (props.links.some((link) => link.path === pathname)) setIsActive(true); - if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true); - }, [isActive, pathname, props.links]); - - return ( - link.rules ?? [])}> -
-
-
- - {props.text} - - {isSubmenuOpened ? : } -
-
- {isSubmenuOpened && ( -
- {props.links.map((link) => ( - - - - ))} -
- )} -
-
- - ); -} diff --git a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/classes.module.scss b/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/classes.module.scss index da462747..cbb69193 100644 --- a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/classes.module.scss +++ b/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/classes.module.scss @@ -1,21 +1,22 @@ @import "@Themes/constants.scss"; .root { + position: absolute; + top: var(--header-height); + left: 0; + + width: 100%; + max-height: calc(100vh - var(--header-height)); + padding: var(--spacing-05, 4px) var(--spacing-2, 16px); + display: flex; flex-direction: column; - background-color: var(--color-generic-white); - box-shadow: $shadow-nav; - padding: 24px; - position: absolute; - top: 83px; - width: 100%; - left: 0; - text-align: center; - max-height: calc(100vh - 83px); + overflow: auto; - > *:not(:last-child) { - margin-bottom: 24px; - } + + border-radius: var(--menu-radius, 0px); + background: var(--color-generic-white, #FFF); + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.10); .separator { width: 100%; diff --git a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/index.tsx b/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/index.tsx index ede9dd10..983abab0 100644 --- a/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/index.tsx +++ b/src/front/Components/DesignSystem/Header/BurgerMenu/BurgerModal/index.tsx @@ -1,170 +1,196 @@ +import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule"; import LogOutButton from "@Front/Components/DesignSystem/LogOutButton"; +import MenuItem from "@Front/Components/DesignSystem/Menu/MenuItem"; +import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; import Module from "@Front/Config/Module"; import React from "react"; -import NavigationLink from "../../NavigationLink"; import classes from "./classes.module.scss"; -import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule"; -import BurgerModalSubmenu from "./BurgerModalSubmenu"; -import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; type IProps = { isOpen: boolean; closeModal: () => void; }; -type IState = {}; -export default class BurgerModal extends React.Component { - // TODO isEnabled depending on role given by DB - public override render(): JSX.Element | null { - if (!this.props.isOpen) return null; - return ( - <> -
-
- - <> - +
+
+ + <> + - -
- - + ], + link: Module.getInstance().get().modules.pages.Folder.props.path, + }} + /> - - -
- - - - - -
- - ); - } + + + + + + + + + + + + + + + + + +
+ + ); } diff --git a/src/front/Components/DesignSystem/Header/HeaderSubmenu/HeaderSubmenuLink/index.tsx b/src/front/Components/DesignSystem/Header/HeaderSubmenu/HeaderSubmenuLink/index.tsx deleted file mode 100644 index 214feb88..00000000 --- a/src/front/Components/DesignSystem/Header/HeaderSubmenu/HeaderSubmenuLink/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Link from "next/link"; -import { useRouter } from "next/router"; -import React, { useEffect } from "react"; - -import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; -import useHoverable from "@Front/Hooks/useHoverable"; - -type IHeaderLinkProps = { - text: string | JSX.Element; - path: string; - routesActive?: string[]; -}; - -export default function HeaderSubmenuLink(props: IHeaderLinkProps) { - const router = useRouter(); - const { pathname } = router; - const [isActive, setIsActive] = React.useState(props.path === pathname); - const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable(); - - useEffect(() => { - if (props.path === pathname) setIsActive(true); - if (props.routesActive) { - for (const routeActive of props.routesActive) { - if (isActive) break; - if (pathname.includes(routeActive)) setIsActive(true); - } - } - }, [isActive, pathname, props.path, props.routesActive]); - - return ( - - - {props.text} - - - ); -} diff --git a/src/front/Components/DesignSystem/Header/HeaderSubmenu/classes.module.scss b/src/front/Components/DesignSystem/Header/HeaderSubmenu/classes.module.scss deleted file mode 100644 index 0e84d6a6..00000000 --- a/src/front/Components/DesignSystem/Header/HeaderSubmenu/classes.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -@import "@Themes/constants.scss"; - -.root { - display: flex; - position: relative; - width: fit-content; - margin: auto; - height: 83px; - padding: 10px 16px; - .content { - margin: auto; - } - .underline { - width: 100%; - height: 3px; - background-color: var(--color-generic-white); - position: absolute; - bottom: 0; - left: 0; - - &[data-active="true"] { - background-color: var(--color-generic-black); - } - } - - &.desactivated { - cursor: not-allowed; - } - - .sub-menu { - box-shadow: 0px 8px 10px 0px #00000012; - padding: 24px; - text-align: center; - gap: 24px; - left: 0; - transform: translateX(-25%); - width: 300px; - top: 84px; - display: flex; - flex-direction: column; - background: white; - position: absolute; - } -} diff --git a/src/front/Components/DesignSystem/Header/HeaderSubmenu/index.tsx b/src/front/Components/DesignSystem/Header/HeaderSubmenu/index.tsx deleted file mode 100644 index 1f6a939d..00000000 --- a/src/front/Components/DesignSystem/Header/HeaderSubmenu/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import classNames from "classnames"; -import { useRouter } from "next/router"; -import React, { useEffect, useState } from "react"; -import { IHeaderLinkProps } from "../ButtonHeader"; - -import Typography, { ETypo, ETypoColor } from "../../Typography"; -import classes from "./classes.module.scss"; -import useHoverable from "@Front/Hooks/useHoverable"; -import HeaderSubmenuLink from "./HeaderSubmenuLink"; -import { IAppRule } from "@Front/Api/Entities/rule"; -import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; - -type IProps = { - text: string | JSX.Element; - links: (IHeaderLinkProps & { - rules?: IAppRule[]; - })[]; -}; - -export default function HeaderSubmenu(props: IProps) { - const router = useRouter(); - const { pathname } = router; - const [isActive, setIsActive] = useState(false); - const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable(100); - - useEffect(() => { - setIsActive(false); - if (props.links.some((link) => link.path === pathname)) setIsActive(true); - if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true); - }, [isActive, pathname, props.links]); - - return ( - link.rules ?? [])}> -
-
-
- - {props.text} - -
-
- {isHovered && ( -
- {props.links.map((link) => ( - - - - ))} -
- )} -
-
- - ); -} diff --git a/src/front/Components/DesignSystem/Header/Navigation/index.tsx b/src/front/Components/DesignSystem/Header/Navigation/index.tsx index 5e4244d3..e6812a0f 100644 --- a/src/front/Components/DesignSystem/Header/Navigation/index.tsx +++ b/src/front/Components/DesignSystem/Header/Navigation/index.tsx @@ -8,7 +8,8 @@ import { AdjustmentsVerticalIcon, BanknotesIcon, Square3Stack3DIcon, TagIcon, Us import { usePathname } from "next/navigation"; import React, { useCallback, useEffect } from "react"; -import Menu, { IItem } from "../../Menu"; +import Menu from "../../Menu"; +import { IItem } from "../../Menu/MenuItem"; import ButtonHeader from "../ButtonHeader"; import classes from "./classes.module.scss"; @@ -175,6 +176,7 @@ const officeItems: IItem[] = [ icon: , text: "RIB Office", link: Module.getInstance().get().modules.pages.OfficesRib.props.path, + routesActive: [Module.getInstance().get().modules.pages.OfficesRib.props.path], rules: [ { action: AppRuleActions.update, diff --git a/src/front/Components/DesignSystem/Header/NavigationLink/classes.module.scss b/src/front/Components/DesignSystem/Header/NavigationLink/classes.module.scss deleted file mode 100644 index 27c75060..00000000 --- a/src/front/Components/DesignSystem/Header/NavigationLink/classes.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import "@Themes/constants.scss"; - -.root { - display: flex; - position: relative; - width: fit-content; - margin: auto; - - .content { - align-content: center; - } -} diff --git a/src/front/Components/DesignSystem/Header/NavigationLink/index.tsx b/src/front/Components/DesignSystem/Header/NavigationLink/index.tsx deleted file mode 100644 index 6972f93f..00000000 --- a/src/front/Components/DesignSystem/Header/NavigationLink/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from "react"; -import classes from "./classes.module.scss"; -import Link from "next/link"; -import classNames from "classnames"; -import { useRouter } from "next/router"; -import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; - -type IProps = { - text: string | JSX.Element; - path?: string; - onClick?: () => void; - isEnabled?: boolean; - isActive?: boolean; - routesActive?: string[]; - target?: "blank" | "self" | "_blank"; -}; - -type IPropsClass = IProps; -type IStateClass = {}; - -class NavigationLinkClass extends React.Component { - static defaultProps = { isEnabled: true }; - public override render(): JSX.Element | null { - if (!this.props.isEnabled) return null; - return ( - -
- - {this.props.text} - -
- - ); - } -} - -export default function NavigationLink(props: IProps) { - const router = useRouter(); - const { pathname } = router; - let isActive = props.path === pathname; - if (props.routesActive) { - for (const routeActive of props.routesActive) { - if (isActive) break; - isActive = pathname.includes(routeActive); - } - } - return ; -} diff --git a/src/front/Components/DesignSystem/Header/Profile/ProfileModal/classes.module.scss b/src/front/Components/DesignSystem/Header/Profile/ProfileModal/classes.module.scss index d2eabeed..1745bc20 100644 --- a/src/front/Components/DesignSystem/Header/Profile/ProfileModal/classes.module.scss +++ b/src/front/Components/DesignSystem/Header/Profile/ProfileModal/classes.module.scss @@ -1,14 +1,26 @@ @import "@Themes/constants.scss"; .root { - display: flex; - flex-direction: column; - background-color: var(--color-generic-white); - box-shadow: $shadow-nav; - padding: 24px; position: absolute; - top: 107px; - right: 66px; + top: 48px; + + display: inline-flex; + flex-direction: column; + align-items: flex-start; + + padding: var(--spacing-05, 4px) var(--spacing-2, 16px); + + border-radius: var(--menu-radius, 0); + border: 1px solid var(--menu-border, #d7dce0); + background: var(--color-generic-white, #fff); + + text-wrap: nowrap; + + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); + z-index: 3; + top: calc(var(--header-height) + 10px); + right: 32px; + text-align: center; animation: smooth-appear 0.2s ease forwards; @@ -20,15 +32,6 @@ opacity: 1; } } - - > *:not(:last-child) { - margin-bottom: 24px; - } - - .separator { - width: 100%; - border: 1px solid var(--color-neutral-200); - } } .background { diff --git a/src/front/Components/DesignSystem/Header/Profile/ProfileModal/index.tsx b/src/front/Components/DesignSystem/Header/Profile/ProfileModal/index.tsx index 3a815e2e..535151e9 100644 --- a/src/front/Components/DesignSystem/Header/Profile/ProfileModal/index.tsx +++ b/src/front/Components/DesignSystem/Header/Profile/ProfileModal/index.tsx @@ -2,8 +2,8 @@ import LogOutButton from "@Front/Components/DesignSystem/LogOutButton"; import Module from "@Front/Config/Module"; import React from "react"; -import NavigationLink from "../../NavigationLink"; import classes from "./classes.module.scss"; +import MenuItem from "@Front/Components/DesignSystem/Menu/MenuItem"; type IProps = { isOpen: boolean; @@ -19,10 +19,28 @@ export default class ProfileModal extends React.Component { <>
- - - -
+ + + + +
diff --git a/src/front/Components/DesignSystem/Header/classes.module.scss b/src/front/Components/DesignSystem/Header/classes.module.scss index 929a37be..ee508b5a 100644 --- a/src/front/Components/DesignSystem/Header/classes.module.scss +++ b/src/front/Components/DesignSystem/Header/classes.module.scss @@ -6,7 +6,7 @@ align-items: center; flex-shrink: 0; - height: 75px; + height: var(--header-height); padding: 0px var(--spacing-lg, 24px); border-bottom: 1px solid var(--menu-border, #d7dce0); diff --git a/src/front/Components/DesignSystem/Header/index.tsx b/src/front/Components/DesignSystem/Header/index.tsx index b0efedca..addada55 100644 --- a/src/front/Components/DesignSystem/Header/index.tsx +++ b/src/front/Components/DesignSystem/Header/index.tsx @@ -23,6 +23,8 @@ type IProps = { isUserConnected: boolean; }; +const headerHeight = 75; + export default function Header(props: IProps) { const { isUserConnected } = props; @@ -44,6 +46,7 @@ export default function Header(props: IProps) { }, []); useEffect(() => { + document.documentElement.style.setProperty("--header-height", `${headerHeight}px`); loadSubscription(); }, [loadSubscription]); diff --git a/src/front/Components/DesignSystem/LogOutButton/index.tsx b/src/front/Components/DesignSystem/LogOutButton/index.tsx index a4f3bcd1..6e1b1e13 100644 --- a/src/front/Components/DesignSystem/LogOutButton/index.tsx +++ b/src/front/Components/DesignSystem/LogOutButton/index.tsx @@ -1,27 +1,20 @@ -import React from "react"; -import Image from "next/image"; -import DisconnectIcon from "@Assets/Icons/disconnect.svg"; -import classes from "./classes.module.scss"; -import Typography, { ETypo, ETypoColor } from "../Typography"; -import { useRouter } from "next/router"; -import UserStore from "@Front/Stores/UserStore"; import { FrontendVariables } from "@Front/Config/VariablesFront"; +import UserStore from "@Front/Stores/UserStore"; +import { PowerIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/router"; +import React, { useCallback } from "react"; + +import MenuItem from "../Menu/MenuItem"; export default function LogOut() { const router = useRouter(); const variables = FrontendVariables.getInstance(); - const disconnect = async () => { - await UserStore.instance.disconnect(); - router.push(`https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=${variables.FRONT_APP_HOST}`); - }; + const disconnect = useCallback(() => { + UserStore.instance + .disconnect() + .then(() => router.push(`https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=${variables.FRONT_APP_HOST}`)); + }, [router, variables.FRONT_APP_HOST]); - return ( -
- - Déconnexion - - disconnect -
- ); + return , onClick: disconnect }} />; } diff --git a/src/front/Components/DesignSystem/Menu/MenuItem/classes.module.scss b/src/front/Components/DesignSystem/Menu/MenuItem/classes.module.scss index 715b9ccf..9e952ad8 100644 --- a/src/front/Components/DesignSystem/Menu/MenuItem/classes.module.scss +++ b/src/front/Components/DesignSystem/Menu/MenuItem/classes.module.scss @@ -1,5 +1,6 @@ .root { width: 100%; + .menu-item { display: flex; padding: var(--spacing-md, 16px); @@ -7,12 +8,16 @@ align-items: center; gap: var(--spacing-lg, 24px); cursor: pointer; + } - > svg { - width: 24px; - height: 24px; - transition: all ease-in-out 0.1s; - } + svg { + width: 24px; + height: 24px; + transition: transform 0.3s ease-in-out; + } + + .chevron.open { + transform: rotate(180deg); } .separator { @@ -20,4 +25,14 @@ height: 1px; background-color: var(--separator-stroke-light, #d7dce0); } + + .dropdown { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-in-out; + } + + .dropdown.open { + max-height: 500px; + } } diff --git a/src/front/Components/DesignSystem/Menu/MenuItem/index.tsx b/src/front/Components/DesignSystem/Menu/MenuItem/index.tsx index 72e0c830..3ca577ff 100644 --- a/src/front/Components/DesignSystem/Menu/MenuItem/index.tsx +++ b/src/front/Components/DesignSystem/Menu/MenuItem/index.tsx @@ -3,27 +3,71 @@ import classes from "./classes.module.scss"; import { useRouter } from "next/router"; import React, { useCallback, useEffect } from "react"; import useHoverable from "@Front/Hooks/useHoverable"; -import { IItem } from ".."; import classNames from "classnames"; +import { IAppRule } from "@Front/Api/Entities/rule"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import useOpenable from "@Front/Hooks/useOpenable"; type IProps = { item: IItem; - closeMenuCb: () => void; }; + +type IItemBase = { + text: string; + icon?: JSX.Element; + hasSeparator?: boolean; + color?: ETypoColor; + onClose?: () => void; +}; + +type IItemWithLink = IItemBase & { + link: string; + rules?: IAppRule[]; + routesActive?: string[]; + onClick?: never; + dropdown?: never; + target?: "_blank"; +}; + +type IItemWithOnClick = IItemBase & { + onClick: () => void; + link?: never; + rules?: never; + routesActive?: never; + dropdown?: never; + target?: never; +}; + +type IItemWithDropdown = IItemBase & { + dropdown: { + items: IItem[]; + }; + routesActive?: never; + link?: never; + rules?: never; + onClick?: never; + target?: never; +}; + +export type IItem = IItemWithLink | IItemWithOnClick | IItemWithDropdown; + export default function MenuItem(props: IProps) { - const { item, closeMenuCb } = props; + const { item } = props; const router = useRouter(); const { pathname } = router; const [isActive, setIsActive] = React.useState(item.link === pathname); + const { isOpen, toggle, open } = useOpenable(); + const handleClickElement = useCallback( (e: React.MouseEvent) => { - closeMenuCb(); + item.onClose?.(); const link = e.currentTarget.getAttribute("data-link"); + if (item.target === "_blank") window.open(item.link, "_blank"); if (link) router.push(link); if (item.onClick) item.onClick(); }, - [closeMenuCb, item, router], + [item, router], ); const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable(); @@ -44,7 +88,25 @@ export default function MenuItem(props: IProps) { if (pathname.includes(routeActive)) setIsActive(true); } } - }, [isActive, item.link, item.routesActive, pathname]); + if (item.dropdown) { + for (const subItem of item.dropdown.items) { + if (isActive) break; + if (subItem.link === pathname) { + !isOpen && open(); + setIsActive(true); + } + if (subItem.routesActive) { + for (const routeActive of subItem.routesActive) { + if (isActive) break; + if (pathname.includes(routeActive)) { + !isOpen && open(); + setIsActive(true); + } + } + } + } + } + }, [isActive, isOpen, item.dropdown, item.link, item.routesActive, open, pathname]); return (
-
- {React.cloneElement(item.icon, { color: `var(${getColor()})` })} +
+ {item.icon && React.cloneElement(item.icon, { color: `var(${getColor()})` })} {item.text} + {item.dropdown && + React.cloneElement(, { + color: `var(${getColor()})`, + })}
+ {item.dropdown && ( +
+ {item.dropdown.items.map((subItem, index) => ( + + ))} +
+ )} {item.hasSeparator &&
}
); diff --git a/src/front/Components/DesignSystem/Menu/index.tsx b/src/front/Components/DesignSystem/Menu/index.tsx index f0dca424..891b978c 100644 --- a/src/front/Components/DesignSystem/Menu/index.tsx +++ b/src/front/Components/DesignSystem/Menu/index.tsx @@ -1,35 +1,10 @@ -import { IAppRule } from "@Front/Api/Entities/rule"; -import { ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; import useHoverable from "@Front/Hooks/useHoverable"; import useOpenable from "@Front/Hooks/useOpenable"; import React, { useEffect, useRef } from "react"; import classes from "./classes.module.scss"; -import MenuItem from "./MenuItem"; - -type IItemBase = { - icon: JSX.Element; - text: string; - hasSeparator?: boolean; - color?: ETypoColor; -}; - -type IItemWithLink = IItemBase & { - link: string; - rules?: IAppRule[]; - routesActive?: string[]; - onClick?: never; -}; - -type IItemWithOnClick = IItemBase & { - onClick: () => void; - link?: never; - rules?: never; - routesActive?: never; -}; - -export type IItem = IItemWithLink | IItemWithOnClick; +import MenuItem, { IItem } from "./MenuItem"; type IProps = { children: React.ReactNode; @@ -79,7 +54,7 @@ export default function Menu(props: IProps) { {items.map((item, index) => { return ( - + ); })} diff --git a/src/front/Components/DesignSystem/MultiSelect/index.tsx b/src/front/Components/DesignSystem/MultiSelect/index.tsx index 535d36c2..d505f4f6 100644 --- a/src/front/Components/DesignSystem/MultiSelect/index.tsx +++ b/src/front/Components/DesignSystem/MultiSelect/index.tsx @@ -1,28 +1,28 @@ +import { ValidationError } from "class-validator"; import classNames from "classnames"; import React from "react"; import ReactSelect, { ActionMeta, MultiValue, Options, PropsValue } from "react-select"; -import { IOption } from "../Form/SelectField"; +import { IOptionOld } from "../Form/SelectFieldOld"; import Typography, { ETypo, ETypoColor } from "../Typography"; import classes from "./classes.module.scss"; import { styles } from "./styles"; -import { ValidationError } from "class-validator"; type IProps = { - options: IOption[]; + options: IOptionOld[]; label?: string | JSX.Element; placeholder?: string; - onChange?: (newValue: MultiValue, actionMeta: ActionMeta) => void; - defaultValue?: PropsValue; - value?: PropsValue; + onChange?: (newValue: MultiValue, actionMeta: ActionMeta) => void; + defaultValue?: PropsValue; + value?: PropsValue; isMulti?: boolean; shouldCloseMenuOnSelect: boolean; - isOptionDisabled?: (option: IOption, selectValue: Options) => boolean; + isOptionDisabled?: (option: IOptionOld, selectValue: Options) => boolean; validationError?: ValidationError; }; type IState = { isFocused: boolean; - selectedOptions: MultiValue; + selectedOptions: MultiValue; validationError: ValidationError | null; }; @@ -124,7 +124,7 @@ export default class MultiSelect extends React.Component { this.setState({ isFocused: false }); } - private onChange(newValue: MultiValue, actionMeta: ActionMeta) { + private onChange(newValue: MultiValue, actionMeta: ActionMeta) { this.props.onChange && this.props.onChange(newValue, actionMeta); this.setState({ selectedOptions: newValue, diff --git a/src/front/Components/DesignSystem/SearchBar/classes.module.scss b/src/front/Components/DesignSystem/SearchBar/classes.module.scss index bfb3e58c..f759e141 100644 --- a/src/front/Components/DesignSystem/SearchBar/classes.module.scss +++ b/src/front/Components/DesignSystem/SearchBar/classes.module.scss @@ -1,6 +1,8 @@ @import "@Themes/constants.scss"; .root { + height: 56px; + display: flex; padding: var(--spacing-2, 16px) var(--spacing-sm, 8px); align-items: flex-start; @@ -10,6 +12,10 @@ border: 1px solid var(--input-main-border-default, #d7dce0); background: var(--input-background, #fff); + svg { + stroke: var(--button-icon-button-default-default); + } + &:hover { border-radius: var(--input-radius, 0px); border: 1px solid var(--input-main-border-hovered, #b4bec5); @@ -28,8 +34,14 @@ background: var(--input-background, #fff); } + &[data-is-disabled="true"] { + opacity: var(--opacity-disabled, 0.3); + pointer-events: none; + } + .input-container { display: flex; + align-items: center; flex: 1; gap: 8px; padding: 0px var(--spacing-2, 16px); diff --git a/src/front/Components/DesignSystem/SearchBar/index.tsx b/src/front/Components/DesignSystem/SearchBar/index.tsx index 57a7af19..113305ce 100644 --- a/src/front/Components/DesignSystem/SearchBar/index.tsx +++ b/src/front/Components/DesignSystem/SearchBar/index.tsx @@ -1,16 +1,18 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect } from "react"; import classes from "./classes.module.scss"; import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; type IProps = { onChange?: (input: string) => void; + value?: string; placeholder?: string; + disabled?: boolean; }; -export default function SearchBar({ onChange, placeholder = "Rechercher" }: IProps) { +export default function SearchBar({ onChange, value: propValue, placeholder = "Rechercher", disabled = false }: IProps) { const [isFocused, setIsFocused] = React.useState(false); - const [value, setValue] = React.useState(""); + const [value, setValue] = React.useState(propValue || ""); const changeValue = useCallback( (value: string) => { @@ -25,8 +27,14 @@ export default function SearchBar({ onChange, placeholder = "Rechercher" }: IPro const handleBlur = () => setIsFocused(false); const clearValue = () => changeValue(""); + useEffect(() => { + if (propValue !== undefined) { + setValue(propValue); + } + }, [propValue]); + return ( -