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/Assets/Icons/coffre.svg b/src/front/Assets/Icons/coffre.svg deleted file mode 100644 index 78198721..00000000 --- a/src/front/Assets/Icons/coffre.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/front/Assets/Icons/question-mark-circle.svg b/src/front/Assets/Icons/question-mark-circle.svg new file mode 100644 index 00000000..94ad2cbd --- /dev/null +++ b/src/front/Assets/Icons/question-mark-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/front/Assets/Icons/vertical-separator.svg b/src/front/Assets/Icons/vertical-separator.svg new file mode 100644 index 00000000..0f2bf83f --- /dev/null +++ b/src/front/Assets/Icons/vertical-separator.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file 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..a5169574 --- /dev/null +++ b/src/front/Components/DesignSystem/Autocomplete/index.tsx @@ -0,0 +1,94 @@ +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"; +import { getLabel } from "../Dropdown"; + +type IProps = { + options: IOption[]; + placeholder?: string; + disabled?: boolean; + label?: string; + onSelectionChange?: (option: IOption | null) => void; + selectedOption?: IOption | null; +}; + +export default function Autocomplete(props: IProps) { + const { onSelectionChange, 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], + ); + + const handleChange = useCallback( + (option: IOption | null) => { + setSelectedOption(option); + onSelectionChange?.(option); + }, + [onSelectionChange], + ); + + useEffect(() => { + setSelectedOption(selectedOptionProps ?? null); + }, [selectedOptionProps]); + + const handleSelectOption = useCallback( + (newOption: IOption, _options: IOption[]) => { + handleChange(newOption); + setSearchValue(getLabel(newOption) || ""); + openable.close(); + }, + [handleChange, openable], + ); + + return ( + +
+ {label && ( + + {label} + + )} +
+ handleChange(null)} + onFocus={openable.open} + /> +
+ ); +} diff --git a/src/front/Components/DesignSystem/AutocompleteMultiSelect/Chip/classes.module.scss b/src/front/Components/DesignSystem/AutocompleteMultiSelect/Chip/classes.module.scss new file mode 100644 index 00000000..f27024cd --- /dev/null +++ b/src/front/Components/DesignSystem/AutocompleteMultiSelect/Chip/classes.module.scss @@ -0,0 +1,26 @@ +@import "@Themes/constants.scss"; + +.root { + width: fit-content; + height: 32px; + + display: inline-flex; + padding: 4px 12px; + align-items: center; + gap: 8px; + + border-radius: var(--input-chip-radius, 360px); + border: 1px solid var(--input-chip-default-border, #b7d1f1); + background: var(--input-chip-default-background, #e5eefa); + + &:hover { + background-color: var(--input-chip-hovered-background); + border-color: var(--input-chip-hovered-border); + } + + .label{ + display: flex; + align-items: center; + gap: 4px; + } +} diff --git a/src/front/Components/DesignSystem/AutocompleteMultiSelect/Chip/index.tsx b/src/front/Components/DesignSystem/AutocompleteMultiSelect/Chip/index.tsx new file mode 100644 index 00000000..42b792f8 --- /dev/null +++ b/src/front/Components/DesignSystem/AutocompleteMultiSelect/Chip/index.tsx @@ -0,0 +1,48 @@ +import { XMarkIcon } from "@heroicons/react/24/outline"; +import classNames from "classnames"; +import React from "react"; + +import IconButton from "../../IconButton"; +import Typography, { ETypo, ETypoColor } from "../../Typography"; +import classes from "./classes.module.scss"; +import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption"; + +type IProps = { + option: IOption; + className?: string; + onDelete?: () => void; +}; + +export default function Chip(props: IProps) { + const { className, option, onDelete } = props; + + return ( +
+ + {getLabelContent(option)} + + } onClick={onDelete} /> +
+ ); +} + +function getLabelContent(option: IOption) { + if (typeof option.label === "string") { + return ( + + {option.label} + + ); + } + + return ( + + + {`${option.label.text} , `} + + + {option.label.subtext} + + + ); +} diff --git a/src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/classes.module.scss b/src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/classes.module.scss new file mode 100644 index 00000000..c242d71a --- /dev/null +++ b/src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/classes.module.scss @@ -0,0 +1,67 @@ +@import "@Themes/constants.scss"; + +.root { + border-radius: var(--input-radius, 0px); + border: 1px solid var(--input-main-border-default, #6d7e8a); + background: var(--input-background, #fff); + + svg { + stroke: var(--button-icon-button-default-default); + } + + &:hover { + border: 1px solid var(--input-main-border-hovered, #b4bec5); + } + + &[data-has-value="true"] { + border: 1px solid var(--input-main-border-filled, #6d7e8a); + } + + &[data-is-focused="true"] { + border: 1px solid var(--input-main-border-focused, #005bcb); + } + + &[data-is-disabled="true"] { + opacity: var(--opacity-disabled, 0.3); + pointer-events: none; + } + + .content { + display: flex; + align-items: center; + align-content: center; + gap: 16px var(--spacing-2, 16px); + flex-wrap: wrap; + + min-height: 56px; + + padding: var(--spacing-1-5, 12px) var(--spacing-sm, 8px); + + .input { + flex: 1; + border: none; + + color: var(--input-placeholder-filled, #24282e); + + /* text/md/semibold */ + font-family: var(--font-text-family, Poppins); + font-size: 16px; + font-style: normal; + font-weight: var(--font-text-weight-semibold, 600); + line-height: normal; + letter-spacing: 0.08px; + width: 100%; + + &::placeholder { + color: var(--input-placeholder-empty, #6d7e8a); + /* text/md/regular */ + font-family: var(--font-text-family, Poppins); + font-size: 16px; + font-style: normal; + font-weight: var(--font-text-weight-regular, 400); + line-height: normal; + letter-spacing: 0.08px; + } + } + } +} diff --git a/src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/index.tsx b/src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/index.tsx new file mode 100644 index 00000000..ba9472f2 --- /dev/null +++ b/src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/index.tsx @@ -0,0 +1,93 @@ +import React, { useCallback, useEffect } from "react"; + +import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption"; +import Chip from "../Chip"; +import classes from "./classes.module.scss"; + +type IProps = { + selectedOptions: IOption[]; + onSelectedOptionsChange: (options: IOption[]) => void; + onChange?: (input: string) => void; + searchValue?: string; + placeholder?: string; + disabled?: boolean; + onClear?: () => void; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function ChipContainer(props: IProps) { + const { + selectedOptions, + onChange, + searchValue, + placeholder = "Rechercher", + disabled = false, + onFocus, + onBlur, + onSelectedOptionsChange, + } = props; + + const [isFocused, setIsFocused] = React.useState(false); + const [value, setValue] = React.useState(searchValue || ""); + + const changeValue = useCallback( + (value: string) => { + setValue(value); + onChange && onChange(value); + }, + [onChange], + ); + + const handleOnChange = useCallback((event: React.ChangeEvent) => changeValue(event.target.value), [changeValue]); + + const handleFocus = useCallback(() => { + setIsFocused(true); + onFocus?.(); + }, [onFocus]); + + const handleBlur = useCallback( + (e: React.FocusEvent) => { + setIsFocused(false); + onBlur?.(); + }, + [onBlur], + ); + + const onChipDelete = useCallback( + (option: IOption) => { + const newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.id !== option.id); + onSelectedOptionsChange && onSelectedOptionsChange(newSelectedOptions); + }, + [selectedOptions, onSelectedOptionsChange], + ); + + useEffect(() => { + if (searchValue !== undefined) { + setValue(searchValue); + } + }, [searchValue]); + + return ( +
0} + data-is-disabled={disabled}> +
+ {selectedOptions.map((option) => ( + onChipDelete(option)} /> + ))} + +
+
+ ); +} diff --git a/src/front/Components/DesignSystem/AutocompleteMultiSelect/classes.module.scss b/src/front/Components/DesignSystem/AutocompleteMultiSelect/classes.module.scss new file mode 100644 index 00000000..0a331a1d --- /dev/null +++ b/src/front/Components/DesignSystem/AutocompleteMultiSelect/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/AutocompleteMultiSelect/index.tsx b/src/front/Components/DesignSystem/AutocompleteMultiSelect/index.tsx new file mode 100644 index 00000000..865327de --- /dev/null +++ b/src/front/Components/DesignSystem/AutocompleteMultiSelect/index.tsx @@ -0,0 +1,95 @@ +import useOpenable from "@Front/Hooks/useOpenable"; +import { useCallback, useEffect, useState } from "react"; + +import { getLabel } from "../Dropdown"; +import DropdownMenu from "../Dropdown/DropdownMenu"; +import { IOption } from "../Dropdown/DropdownMenu/DropdownOption"; +import Typography, { ETypo, ETypoColor } from "../Typography"; +import ChipContainer from "./ChipContainer"; +import classes from "./classes.module.scss"; + +type IProps = { + options: IOption[]; + placeholder?: string; + disabled?: boolean; + label?: string; + onSelectionChange?: (options: IOption[] | null) => void; + selectedOptions?: IOption[] | null; +}; + +export default function AutocompleteMultiSelect(props: IProps) { + const { onSelectionChange, options, placeholder, disabled, label, selectedOptions: selectedOptionsProps } = props; + const [selectedOptions, setSelectedOptions] = useState(selectedOptionsProps ?? 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())); + 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], + ); + + const handleChange = useCallback( + (options: IOption[] | null) => { + setSelectedOptions(options); + onSelectionChange?.(options); + }, + [onSelectionChange], + ); + + useEffect(() => { + setSelectedOptions(selectedOptionsProps ?? null); + }, [selectedOptionsProps]); + + const handleSelectOption = useCallback( + (_newOption: IOption, options: IOption[]) => { + handleChange(options); + setSearchValue(""); + openable.close(); + }, + [handleChange, openable], + ); + + return ( + +
+ {label && ( + + {label} + + )} +
+ handleChange(null)} + onFocus={openable.open} + selectedOptions={selectedOptions ?? []} + onSelectedOptionsChange={handleChange} + /> +
+ ); +} \ No newline at end of file diff --git a/src/front/Components/DesignSystem/Button/classes.module.scss b/src/front/Components/DesignSystem/Button/classes.module.scss index d367db76..9beda2dd 100644 --- a/src/front/Components/DesignSystem/Button/classes.module.scss +++ b/src/front/Components/DesignSystem/Button/classes.module.scss @@ -677,7 +677,8 @@ text-transform: inherit; } - &[disabled="true"] { + &:disabled { opacity: var(--opacity-disabled, 0.3); + pointer-events: none; } } diff --git a/src/front/Components/DesignSystem/Button/index.tsx b/src/front/Components/DesignSystem/Button/index.tsx index 0bdb4974..73f6789b 100644 --- a/src/front/Components/DesignSystem/Button/index.tsx +++ b/src/front/Components/DesignSystem/Button/index.tsx @@ -2,6 +2,7 @@ import classNames from "classnames"; import React from "react"; import classes from "./classes.module.scss"; +import Loader from "../Loader"; export enum EButtonVariant { PRIMARY = "primary", @@ -57,18 +58,23 @@ export default function Button(props: IButtonProps) { } = props; const fullwidthattr = fullwidth.toString(); - const isloadingattr = isLoading.toString(); - const attributes = { ...props, variant, disabled, type, isloadingattr, fullwidthattr, sizing: size, styletype }; + const attributes = { ...props, variant, disabled, type, fullwidthattr, sizing: size, styletype }; delete attributes.fullwidth; delete attributes.leftIcon; delete attributes.rightIcon; + delete attributes.isLoading; return ( - ); } diff --git a/src/front/Components/DesignSystem/CheckBox/classes.module.scss b/src/front/Components/DesignSystem/CheckBox/classes.module.scss index 5f943fd1..60488bd8 100644 --- a/src/front/Components/DesignSystem/CheckBox/classes.module.scss +++ b/src/front/Components/DesignSystem/CheckBox/classes.module.scss @@ -3,43 +3,77 @@ .root { cursor: pointer; display: flex; - align-items: center; - &.disabled { - cursor: not-allowed; - } + align-items: flex-start; - input[type="checkbox"] { - appearance: none; - background-color: transparent; - width: 16px; - height: 16px; - border: 1px solid var(--color-secondary-500); - border-radius: 2px; - margin-right: 16px; - display: grid; - place-content: center; + color: var(--text-primary, #24282e); + font-size: 16px; + font-style: normal; + font-weight: var(--font-text-weight-regular, 400); + line-height: normal; + letter-spacing: 0.08px; + gap: var(--Radius-lg, 16px); - &:disabled { - cursor: not-allowed; + .content { + cursor: pointer; + display: flex; + flex-direction: column; + .label { + color: var(--text-primary, #24282e); + font-weight: var(--font-text-weight-regular, 400); + } + .description { + color: var(--text-secondary, #47535d); + font-weight: var(--font-text-weight-light, 300); } } - input[type="checkbox"]::before { - content: url("../../../Assets/Icons/check_white.svg"); - place-content: flex-start; - display: grid; - width: 16px; - height: 16px; - background-color: var(--color-secondary-500); - border-radius: 2px; - transform: scale(0); + input[type="checkbox"] { + accent-color: var(--select-option-selected-default-background); + cursor: pointer; + display: flex; + width: 20px; + height: 20px; + min-width: 20px; + max-width: 20px; + flex-direction: column; + align-items: flex-start; + margin-right: 16px; + gap: 8px; + border-radius: var(--radius-xs, 2px); + border: 2px solid var(--select-option-unselected-default-border, #6d7e8a); + + &:hover { + border: 2px solid var(--select-option-unselected-pressed-border, #3e474e); + accent-color: var(--select-option-selected-hovered-background); + } + + &:active { + border: 2px solid var(--select-option-unselected-pressed-border, #3e474e); + accent-color: var(--select-option-selected-pressed-background); + } } - input[type="checkbox"]:checked::before { - transform: scale(1); + &.disabled { + pointer-events: none; + cursor: not-allowed; + opacity: var(--opacity-disabled, 0.3); } - .tooltip { - margin-left: 16px; - } + // input[type="checkbox"]::before { + // content: url("../../../Assets/Icons/check_white.svg"); + // place-content: flex-start; + // display: grid; + // width: 16px; + // height: 16px; + // background-color: var(--color-secondary-500); + // border-radius: 2px; + // transform: scale(0); + // } + + // input[type="checkbox"]:checked::before { + // transform: scale(1); + // } + + // .tooltip { + // } } diff --git a/src/front/Components/DesignSystem/CheckBox/index.tsx b/src/front/Components/DesignSystem/CheckBox/index.tsx index 70f2c91a..4ee3c5ba 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; @@ -45,9 +45,20 @@ export default class CheckBox extends React.Component { value={this.props.option.value as string} onChange={this.onChange} checked={this.state.checked} - disabled={this.props.disabled} /> - {this.props.option.label} +
+ {this.props.option.label && ( + + {this.props.option.label} + + )} + {this.props.option.description && ( + + {this.props.option.description} + + )} +
+ {this.props.toolTip && } 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..a56ca26e --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/classes.module.scss @@ -0,0 +1,37 @@ +.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; + overflow: auto; + 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..5ad68d7b --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/DropdownMenu/index.tsx @@ -0,0 +1,63 @@ +import classNames from "classnames"; +import React, { useCallback, useEffect, useRef } from "react"; + +import classes from "./classes.module.scss"; +import DropdownOption, { IOption } from "./DropdownOption"; + +type IProps = { + options: IOption[]; + selectedOptions: IOption[]; + children: React.ReactNode; + openable: { + isOpen: boolean; + open: () => void; + close: () => void; + toggle: () => void; + }; + onSelect?: (newOption: IOption, options: IOption[]) => void; +}; +export default function DropdownMenu(props: IProps) { + const { children, options, onSelect, openable, selectedOptions } = props; + const ref = useRef(null); + + const handleSelect = useCallback( + (option: IOption) => { + const newOptions = selectedOptions.some((selectedOption) => selectedOption.id === option.id) + ? selectedOptions + : [...selectedOptions, option]; + + onSelect?.(option, newOptions); + openable.close(); + }, + [onSelect, openable, selectedOptions], + ); + + const handleClickOutside = useCallback( + (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + openable.close(); + } + }, + [openable], + ); + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [handleClickOutside]); + + return ( +
+ {children} +
+ {options.map((option) => { + return ; + })} +
+
+ ); + + function isActive(option: IOption): boolean { + return selectedOptions.some((selectedOption) => 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..0ce12d9d --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/classes.module.scss @@ -0,0 +1,65 @@ +@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; + gap: 4px; + 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..b88dae07 --- /dev/null +++ b/src/front/Components/DesignSystem/Dropdown/index.tsx @@ -0,0 +1,112 @@ +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; + onSelectionChange?: (option: IOption) => void; + selectedOption?: IOption | null; +}; + +export default function Dropdown(props: IProps) { + const { options, placeholder, disabled, onSelectionChange, selectedOption: selectedOptionProps, label } = props; + const [selectedOption, setSelectedOption] = useState(selectedOptionProps ?? null); + const openable = useOpenable({ defaultOpen: false }); + + useEffect(() => { + setSelectedOption(selectedOptionProps ?? null); + }, [selectedOptionProps]); + + const handleOnSelect = useCallback( + (newOption: IOption, _options: IOption[]) => { + setSelectedOption(newOption); + onSelectionChange?.(newOption); + }, + [onSelectionChange], + ); + + return ( + +
+ {label && ( + + {label} + + )} +
openable.toggle()}> +
+ {getLabelContent(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}`; +} + +function getLabelContent(option: IOption | null, placeholder?: string) { + if (!option) + return ( + + {placeholder} + + ); + + if (typeof option.label === "string") { + return ( + + {option.label} + + ); + } + + return ( + + + {`${option.label.text} , `} + + + {option.label.subtext} + + + ); +} diff --git a/src/front/Components/DesignSystem/FolderList/classes.module.scss b/src/front/Components/DesignSystem/FolderList/classes.module.scss deleted file mode 100644 index b1bf1c69..00000000 --- a/src/front/Components/DesignSystem/FolderList/classes.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import "@Themes/constants.scss"; - -.root { - height: calc(100vh - 290px); - overflow-y: scroll; - - &.archived { - height: calc(100vh - 220px); - } - - .active { - background-color: var(--color-neutral-200); - } -} diff --git a/src/front/Components/DesignSystem/FolderList/index.tsx b/src/front/Components/DesignSystem/FolderList/index.tsx deleted file mode 100644 index c319582a..00000000 --- a/src/front/Components/DesignSystem/FolderList/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import Module from "@Front/Config/Module"; -import classNames from "classnames"; -import { OfficeFolder } from "le-coffre-resources/dist/Notary"; -import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import React from "react"; - -import FolderContainer from "../FolderContainer"; -import classes from "./classes.module.scss"; - -type IProps = { - folders: OfficeFolder[]; - isArchived: boolean; - onSelectedFolder?: (folder: OfficeFolder) => void; - onCloseLeftSide?: () => void; -}; - -type IPropsClass = IProps & { - selectedFolder: string; -}; - -type IState = {}; - -class FolderListClass extends React.Component { - private redirectPath: string = this.props.isArchived - ? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path - : Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path; - public override render(): JSX.Element { - return
{this.renderFolders()}
; - } - - private renderFolders(): JSX.Element[] { - const pendingFolders = this.props.folders - .filter((folder) => { - const pendingDocuments = (folder.documents ?? []).filter( - (document) => document.document_status === EDocumentStatus.DEPOSITED, - ); - return pendingDocuments.length >= 1; - }) - .sort((folder1, folder2) => { - return folder1.created_at! > folder2.created_at! ? -1 : 1; - }); - - const otherFolders = this.props.folders - .filter((folder) => { - const pendingDocuments = (folder.documents ?? []).filter( - (document) => document.document_status === EDocumentStatus.DEPOSITED, - ); - return pendingDocuments.length === 0; - }) - .sort((folder1, folder2) => { - return folder1.created_at! > folder2.created_at! ? -1 : 1; - }); - - return [...pendingFolders, ...otherFolders].map((folder) => { - return ( -
- - ; - -
- ); - }); - } -} - -export default function FolderList(props: IProps) { - const router = useRouter(); - let { folderUid } = router.query; - folderUid = folderUid as string; - return ; -} diff --git a/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss b/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss deleted file mode 100644 index deb6e813..00000000 --- a/src/front/Components/DesignSystem/FolderListContainer/classes.module.scss +++ /dev/null @@ -1,23 +0,0 @@ -@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/FolderListContainer/index.tsx b/src/front/Components/DesignSystem/FolderListContainer/index.tsx deleted file mode 100644 index 53df5b74..00000000 --- a/src/front/Components/DesignSystem/FolderListContainer/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import Module from "@Front/Config/Module"; -import { OfficeFolder } from "le-coffre-resources/dist/Notary"; -import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document"; -import { useRouter } from "next/router"; -import React, { useCallback, useEffect } from "react"; - -import classes from "./classes.module.scss"; -import { IBlock } from "../SearchBlockList/BlockList/Block"; -import SearchBlockList from "../SearchBlockList"; - -type IProps = { - folders: OfficeFolder[]; - isArchived: boolean; - onSelectedFolder?: (folder: OfficeFolder) => void; - onCloseLeftSide?: () => void; -}; - -export default function FolderListContainer(props: IProps) { - const router = useRouter(); - const { folderUid } = router.query; - const { folders, isArchived } = props; - - const redirectPath: string = isArchived - ? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path - : Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path; - - const getBlocks = useCallback( - (folders: OfficeFolder[]): IBlock[] => { - const pendingFolders = folders - .filter((folder) => { - const pendingDocuments = (folder.documents ?? []).filter( - (document) => document.document_status === EDocumentStatus.DEPOSITED, - ); - return pendingDocuments.length >= 1; - }) - .sort((folder1, folder2) => { - return folder1.created_at! > folder2.created_at! ? -1 : 1; - }); - - const otherFolders = folders - .filter((folder) => { - const pendingDocuments = (folder.documents ?? []).filter( - (document) => document.document_status === EDocumentStatus.DEPOSITED, - ); - return pendingDocuments.length === 0; - }) - .sort((folder1, folder2) => { - return folder1.created_at! > folder2.created_at! ? -1 : 1; - }); - - return [...pendingFolders, ...otherFolders].map((folder) => { - return { - id: folder.uid!, - primaryText: folder.name, - secondaryText: folder.folder_number, - isActive: folderUid === folder.uid, - showAlert: folder.documents?.some((document) => document.document_status === EDocumentStatus.DEPOSITED), - }; - }); - }, - [folderUid], - ); - - const [blocks, setBlocks] = React.useState(getBlocks(folders)); - - const onSelectedFolder = (block: IBlock) => { - props.onCloseLeftSide && props.onCloseLeftSide(); - const folder = folders.find((folder) => folder.uid === block.id); - if (!folder) return; - props.onSelectedFolder && props.onSelectedFolder(folder); - const path = redirectPath.replace("[folderUid]", folder.uid ?? ""); - router.push(path); - }; - - useEffect(() => { - setBlocks(getBlocks(folders)); - }, [folders, getBlocks]); - - return ( -
- -
- ); -} diff --git a/src/front/Components/DesignSystem/Footer/classes.module.scss b/src/front/Components/DesignSystem/Footer/classes.module.scss index f9a12433..4155ff54 100644 --- a/src/front/Components/DesignSystem/Footer/classes.module.scss +++ b/src/front/Components/DesignSystem/Footer/classes.module.scss @@ -3,26 +3,33 @@ .root { border-top: 1px solid var(--footer-border, #d7dce0); background: var(--footer-background, #fff); - padding: var(--spacing-1-5) 0; - font-family: var(--font-title-family, Poppins); - font-size: 12px; - font-weight: var(--font-text-weight-regular, 400); - letter-spacing: 0.06px; .sub-root { display: flex; align-items: center; gap: var(--Radius-lg, 16px); white-space: nowrap; - padding: 0 360px; - //make it sticky + } - @media (max-width: 1023px) { - padding: 0 12px; + .desktop { + padding: var(--spacing-1-5, 12px) var(--Radius-xl, 24px); + } + + .tablet { + padding: var(--spacing-1-5, 12px) var(--Radius-xl, 24px); + } + + .mobile { + padding: var(--spacing-1-5, 12px) var(--Radius-lg, 16px); + } + + &[data-has-left-padding="true"] { + .desktop { + padding: var(--spacing-1-5, 12px) var(--spacing-15, 120px); } } - @media (max-width: 660px) or (min-width: 768px) { + @media (max-width: 660px) or (min-width: 1023px) { .tablet { display: none; } @@ -34,7 +41,7 @@ } } - @media (max-width: 769px) { + @media (max-width: 1023px) { .desktop { display: none; } @@ -45,4 +52,11 @@ content: "|"; } } + a { + color: var(--footer-contrast, #47535d); + font-family: var(--font-title-family, Poppins); + font-size: 12px; + font-weight: var(--font-text-weight-regular, 400); + letter-spacing: 0.06px; + } } diff --git a/src/front/Components/DesignSystem/Footer/desktop.tsx b/src/front/Components/DesignSystem/Footer/desktop.tsx index d0eb1772..9b4e4c58 100644 --- a/src/front/Components/DesignSystem/Footer/desktop.tsx +++ b/src/front/Components/DesignSystem/Footer/desktop.tsx @@ -1,20 +1,27 @@ import React from "react"; import classes from "./classes.module.scss"; +import Link from "next/link"; +import Module from "@Front/Config/Module"; +import { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard"; +import VerticalSeparator from "@Assets/Icons/vertical-separator.svg"; +import Image from "next/image"; type IProps = { className?: string; }; +const legalPages = Module.getInstance().get().modules.pages.Legal.pages.LegalInformations.props.path; + export default function Desktop({ className }: IProps) { return (
- © Copyright lecoffre 2024 - - Conditions d'utilisation - - Politique de confidentialité - - Politique des cookies + © Copyright lecoffre 2024 + Separator + Juridiques + Separator + Politique de confidentialité + Separator + Politique des cookies
); } diff --git a/src/front/Components/DesignSystem/Footer/index.tsx b/src/front/Components/DesignSystem/Footer/index.tsx index 8867ec7f..084673cb 100644 --- a/src/front/Components/DesignSystem/Footer/index.tsx +++ b/src/front/Components/DesignSystem/Footer/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import classes from "./classes.module.scss"; import Mobile from "./mobile"; import Desktop from "./desktop"; @@ -6,11 +6,18 @@ import Tablet from "./tablet"; type IProps = { className?: string; + hasLeftPadding?: boolean; }; -export default function Footer({ className }: IProps) { +export default function Footer({ className, hasLeftPadding = false }: IProps) { + const footerRef = React.useRef(null); + useEffect(() => { + if (!footerRef.current) return; + const footerHeight = footerRef.current.clientHeight + 1; + document.documentElement.style.setProperty("--footer-height", `${footerHeight}px`); + }); return ( -