From 6a5dcb9fae508fa661461a3cd3354a048f05da13 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Wed, 24 Jul 2024 16:11:41 +0300 Subject: [PATCH 1/8] feature: multiselect menu --- .../MultiselectMenu/Dropdown/Dropdown.less | 30 ++++++++++ .../MultiselectMenu/Dropdown/Dropdown.tsx | 53 ++++++++++++++++++ .../Dropdown/Option/Option.less | 30 ++++++++++ .../Dropdown/Option/Option.tsx | 42 ++++++++++++++ .../MultiselectMenu/Dropdown/Option/index.ts | 5 ++ src/common/MultiselectMenu/Dropdown/index.ts | 5 ++ .../MultiselectMenu/MultiselectMenu.less | 34 +++++++++++ .../MultiselectMenu/MultiselectMenu.tsx | 56 +++++++++++++++++++ src/common/MultiselectMenu/index.ts | 5 ++ src/common/MultiselectMenu/types.d.ts | 9 +++ src/common/index.js | 2 + .../VideosList/SeasonsBar/SeasonsBar.js | 14 ++--- 12 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 src/common/MultiselectMenu/Dropdown/Dropdown.less create mode 100644 src/common/MultiselectMenu/Dropdown/Dropdown.tsx create mode 100644 src/common/MultiselectMenu/Dropdown/Option/Option.less create mode 100644 src/common/MultiselectMenu/Dropdown/Option/Option.tsx create mode 100644 src/common/MultiselectMenu/Dropdown/Option/index.ts create mode 100644 src/common/MultiselectMenu/Dropdown/index.ts create mode 100644 src/common/MultiselectMenu/MultiselectMenu.less create mode 100644 src/common/MultiselectMenu/MultiselectMenu.tsx create mode 100644 src/common/MultiselectMenu/index.ts create mode 100644 src/common/MultiselectMenu/types.d.ts diff --git a/src/common/MultiselectMenu/Dropdown/Dropdown.less b/src/common/MultiselectMenu/Dropdown/Dropdown.less new file mode 100644 index 000000000..1e15419ef --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Dropdown.less @@ -0,0 +1,30 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.dropdown { + background: var(--modal-background-color); + display: none; + position: absolute; + width: 100%; + top: 100%; + left: 0; + z-index: 10; + box-shadow: var(--outer-glow); + border-radius: var(--border-radius); + overflow: hidden; + + &.open { + display: block; + } + + .back-button { + display: flex; + align-items: center; + gap: 0 0.5rem; + padding: 0.75rem; + color: var(--primary-foreground-color); + + .back-button-icon { + width: 1.5rem; + } + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Dropdown.tsx b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..1c09de925 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx @@ -0,0 +1,53 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import Button from 'stremio/common/Button'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import Option from './Option'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './Dropdown.less'; + +type Props = { + options: MultiselectMenuOption[]; + selectedOption?: MultiselectMenuOption | null; + menuOpen: boolean | (() => void); + level: number; + setLevel: (level: number) => void; + onSelect: (option: MultiselectMenuOption) => void; +}; + +const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => { + const { t } = useTranslation(); + const onBackButtonClick = () => { + setLevel(level - 1); + }; + + return ( +
+ { + level > 0 ? + + : null + } + { + options + .filter((option: MultiselectMenuOption) => !option.hidden) + .map((option: MultiselectMenuOption) => ( +
+ ); +}; + +export default Dropdown; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.less b/src/common/MultiselectMenu/Dropdown/Option/Option.less new file mode 100644 index 000000000..067934dfc --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.less @@ -0,0 +1,30 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.option { + font-size: var(--font-size-normal); + color: var(--primary-foreground-color); + align-items: center; + display: flex; + flex-direction: row; + padding: 1rem; + + .label { + flex: 1; + color: var(--primary-foreground-color); + } + + .icon { + flex: none; + // display: none; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + margin-left: 1rem; + background-color: var(--secondary-accent-color); + opacity: 1; + } + + &:hover { + background-color: rgba(255, 255, 255, 0.15); + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx new file mode 100644 index 000000000..7ade6dfa5 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx @@ -0,0 +1,42 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import Button from 'stremio/common/Button'; +import styles from './Option.less'; +import Icon from '@stremio/stremio-icons/react'; + +type Props = { + option: MultiselectMenuOption; + selectedOption?: MultiselectMenuOption | null; + onSelect: (option: MultiselectMenuOption) => void; +}; + +const Option = ({ option, selectedOption, onSelect }: Props) => { + // consider using option.id === selectedOption?.id instead + const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]); + + return ( + + ); +}; + +export default Option; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/index.ts b/src/common/MultiselectMenu/Dropdown/Option/index.ts new file mode 100644 index 000000000..6004f7754 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Option from './Option'; + +export default Option; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/index.ts b/src/common/MultiselectMenu/Dropdown/index.ts new file mode 100644 index 000000000..ce3622a25 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Dropdown from './Dropdown'; + +export default Dropdown; \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.less b/src/common/MultiselectMenu/MultiselectMenu.less new file mode 100644 index 000000000..156e2035d --- /dev/null +++ b/src/common/MultiselectMenu/MultiselectMenu.less @@ -0,0 +1,34 @@ + +@border-radius: 2.75rem; + +.multiselect-menu { + background-color: var(--overlay-color); + position: relative; + min-width: 150px; + overflow: visible; + border-radius: @border-radius; + &.disabled { + pointer-events: none; + opacity: 0.3; + } + + .multiselect-button { + color: var(--primary-foreground-color); + background-color: rgba(255, 255, 255, 0.05); + padding: 0.75rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0 0.5rem; + border-radius: @border-radius; + + .icon { + width: 1.5rem; + color: var(--primary-foreground-color); + + &.open { + transform: rotate(180deg); + } + } + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.tsx b/src/common/MultiselectMenu/MultiselectMenu.tsx new file mode 100644 index 000000000..5eb073eca --- /dev/null +++ b/src/common/MultiselectMenu/MultiselectMenu.tsx @@ -0,0 +1,56 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import Button from 'stremio/common/Button'; +import useBinaryState from 'stremio/common/useBinaryState'; +import Dropdown from './Dropdown'; +import classNames from 'classnames'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './MultiselectMenu.less'; + +type Props = { + className?: string, + title?: string; + options: MultiselectMenuOption[]; + selectedOption?: MultiselectMenuOption; + onSelect: () => void; +}; + +const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => { + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); + const multiselectMenuRef = React.useRef(null); + const [level, setLevel] = React.useState(0); + + const onOptionSelect = (option: MultiselectMenuOption) => { + option.level ? setLevel(level + 1) : onSelect(), closeMenu(); + }; + + return ( +
+ + { + menuOpen ? + + : null + } +
+ ); +}; + +export default MultiselectMenu; \ No newline at end of file diff --git a/src/common/MultiselectMenu/index.ts b/src/common/MultiselectMenu/index.ts new file mode 100644 index 000000000..e526218cd --- /dev/null +++ b/src/common/MultiselectMenu/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import MultiselectMenu from './MultiselectMenu'; + +export default MultiselectMenu; \ No newline at end of file diff --git a/src/common/MultiselectMenu/types.d.ts b/src/common/MultiselectMenu/types.d.ts new file mode 100644 index 000000000..a1724ab9b --- /dev/null +++ b/src/common/MultiselectMenu/types.d.ts @@ -0,0 +1,9 @@ +type MultiselectMenuOption = { + id?: number; + label: string; + value: string; + destination?: string; + default?: boolean; + hidden?: boolean; + level?: MultiselectMenuOption[]; +}; \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 8046dab15..c582a13ca 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -15,6 +15,7 @@ const MetaPreview = require('./MetaPreview'); const MetaRow = require('./MetaRow'); const ModalDialog = require('./ModalDialog'); const Multiselect = require('./Multiselect'); +const { default: MultiselectMenu } = require('./MultiselectMenu'); const { HorizontalNavBar, VerticalNavBar } = require('./NavBar'); const PaginationInput = require('./PaginationInput'); const PlayIconCircleCentered = require('./PlayIconCircleCentered'); @@ -63,6 +64,7 @@ module.exports = { MetaRow, ModalDialog, Multiselect, + MultiselectMenu, HorizontalNavBar, VerticalNavBar, PaginationInput, diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index d75080d01..6ecc2c815 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -5,9 +5,10 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { t } = require('i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { Button, Multiselect } = require('stremio/common'); +const { Button } = require('stremio/common'); const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder'); const styles = require('./styles'); +const { MultiselectMenu } = require('stremio/common'); const SeasonsBar = ({ className, seasons, season, onSelect }) => { const options = React.useMemo(() => { @@ -16,8 +17,8 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL') })); }, [seasons]); - const selected = React.useMemo(() => { - return [String(season)]; + const selectedSeason = React.useMemo(() => { + return { label: String(season), value: String(season) }; }, [season]); const prevNextButtonOnClick = React.useCallback((event) => { if (typeof onSelect === 'function') { @@ -61,12 +62,11 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
Prev
- 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} - direction={'bottom-left'} options={options} - selected={selected} + title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} + selectedOption={selectedSeason} onSelect={seasonOnSelect} /> : null diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.less b/src/common/MultiselectMenu/Dropdown/Option/Option.less index 067934dfc..a0ee1743f 100644 --- a/src/common/MultiselectMenu/Dropdown/Option/Option.less +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.less @@ -15,7 +15,6 @@ .icon { flex: none; - // display: none; width: 0.5rem; height: 0.5rem; border-radius: 100%; diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx index 28a431b1f..4309e766e 100644 --- a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx @@ -9,7 +9,7 @@ import Icon from '@stremio/stremio-icons/react'; type Props = { option: MultiselectMenuOption; selectedOption?: MultiselectMenuOption | null; - onSelect: (value: string) => void; + onSelect: (value: number) => void; }; const Option = ({ option, selectedOption, onSelect }: Props) => { @@ -32,7 +32,7 @@ const Option = ({ option, selectedOption, onSelect }: Props) => { } { option.level ? - + : null } diff --git a/src/common/MultiselectMenu/MultiselectMenu.less b/src/common/MultiselectMenu/MultiselectMenu.less index 58eec12dd..3c7b81b59 100644 --- a/src/common/MultiselectMenu/MultiselectMenu.less +++ b/src/common/MultiselectMenu/MultiselectMenu.less @@ -3,11 +3,11 @@ @border-radius: 2.75rem; .multiselect-menu { - background-color: var(--overlay-color); position: relative; - min-width: 150px; + min-width: 8.5rem; overflow: visible; border-radius: @border-radius; + &.disabled { pointer-events: none; opacity: 0.3; @@ -15,7 +15,6 @@ .multiselect-button { color: var(--primary-foreground-color); - background-color: rgba(255, 255, 255, 0.05); padding: 0.75rem 1.5rem; display: flex; justify-content: space-between; @@ -24,12 +23,17 @@ border-radius: @border-radius; .icon { - width: 1.5rem; + width: 1rem; color: var(--primary-foreground-color); + opacity: 0.6; &.open { transform: rotate(180deg); } } } + + &:hover { + background-color: var(--overlay-color); + } } \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.tsx b/src/common/MultiselectMenu/MultiselectMenu.tsx index 030b097da..2bd298752 100644 --- a/src/common/MultiselectMenu/MultiselectMenu.tsx +++ b/src/common/MultiselectMenu/MultiselectMenu.tsx @@ -14,7 +14,7 @@ type Props = { title?: string; options: MultiselectMenuOption[]; selectedOption?: MultiselectMenuOption; - onSelect: (event: any) => void; + onSelect: (value: number) => void; }; const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => { @@ -22,9 +22,8 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect } const multiselectMenuRef = useOutsideClick(() => closeMenu()); const [level, setLevel] = React.useState(0); - const onOptionSelect = (event: any) => { - console.log(event.value); - level ? setLevel(level + 1) : onSelect(event), closeMenu(); + const onOptionSelect = (value: number) => { + level ? setLevel(level + 1) : onSelect(value), closeMenu(); }; return ( @@ -37,7 +36,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect } aria-expanded={menuOpen} > {title} - + { menuOpen ? diff --git a/src/common/MultiselectMenu/types.d.ts b/src/common/MultiselectMenu/types.d.ts index a1724ab9b..7ed039ddd 100644 --- a/src/common/MultiselectMenu/types.d.ts +++ b/src/common/MultiselectMenu/types.d.ts @@ -1,7 +1,7 @@ type MultiselectMenuOption = { id?: number; label: string; - value: string; + value: number; destination?: string; default?: boolean; hidden?: boolean; diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index 6ecc2c815..71c10ce92 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -36,8 +36,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { }); } }, [season, seasons, onSelect]); - const seasonOnSelect = React.useCallback((event) => { - const value = parseFloat(event.value); + const seasonOnSelect = React.useCallback((value) => { if (typeof onSelect === 'function') { onSelect({ type: 'select', From a1b94a68ff3ba428f3a31afb3de4cc411c92a18c Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Wed, 18 Sep 2024 16:30:40 +0300 Subject: [PATCH 7/8] refactor: inline function --- src/common/MultiselectMenu/Dropdown/Option/Option.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx index 4309e766e..68884610f 100644 --- a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2024 Smart code 203358507 -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import classNames from 'classnames'; import Button from 'stremio/common/Button'; import styles from './Option.less'; @@ -16,11 +16,15 @@ const Option = ({ option, selectedOption, onSelect }: Props) => { // consider using option.id === selectedOption?.id instead const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]); + const handleClick = useCallback(() => { + onSelect(option.value); + }, [onSelect, option.value]); + return (