From b25abe11870082c267c30cd76d32da7bc5cfdbe8 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Feb 2025 22:47:28 -0300 Subject: [PATCH] Refactor ExplorerFilter --- .../search/components/explorerFilter.tsx | 333 +-------------- src/features/search/components/filters.tsx | 381 ++++++++++++++++++ 2 files changed, 396 insertions(+), 318 deletions(-) create mode 100644 src/features/search/components/filters.tsx diff --git a/src/features/search/components/explorerFilter.tsx b/src/features/search/components/explorerFilter.tsx index 2e12093b5..b9be93f97 100644 --- a/src/features/search/components/explorerFilter.tsx +++ b/src/features/search/components/explorerFilter.tsx @@ -1,22 +1,20 @@ import arrowIcon from '@tabler/icons/outline/chevron-down.svg'; -import searchIcon from '@tabler/icons/outline/search.svg'; -import xIcon from '@tabler/icons/outline/x.svg'; -import clsx from 'clsx'; import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { changeSearch, submitSearch } from 'soapbox/actions/search.ts'; -import Button from 'soapbox/components/ui/button.tsx'; -import Checkbox from 'soapbox/components/ui/checkbox.tsx'; import Divider from 'soapbox/components/ui/divider.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import IconButton from 'soapbox/components/ui/icon-button.tsx'; -import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; -import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import Toggle from 'soapbox/components/ui/toggle.tsx'; -import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; +import { + CreateFilter, + LanguageFilter, + PlatformFilters, + RepliesFilter, + generateFilter, +} from 'soapbox/features/search/components/filters.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; const messages = defineMessages({ @@ -35,72 +33,6 @@ const messages = defineMessages({ addFilter: { id: 'column.explorer.filters.add_filter', defaultMessage: 'Add Filter' }, }); -const languages = { - default: 'Global', - en: 'English', - ar: 'العربية', - ast: 'Asturianu', - bg: 'Български', - bn: 'বাংলা', - ca: 'Català', - co: 'Corsu', - cs: 'Čeština', - cy: 'Cymraeg', - da: 'Dansk', - de: 'Deutsch', - el: 'Ελληνικά', - 'en-Shaw': '𐑖𐑱𐑝𐑾𐑯', - eo: 'Esperanto', - es: 'Español', - eu: 'Euskara', - fa: 'فارسی', - fi: 'Suomi', - fr: 'Français', - ga: 'Gaeilge', - gl: 'Galego', - he: 'עברית', - hi: 'हिन्दी', - hr: 'Hrvatski', - hu: 'Magyar', - hy: 'Հայերեն', - id: 'Bahasa Indonesia', - io: 'Ido', - is: 'íslenska', - it: 'Italiano', - ja: '日本語', - jv: 'ꦧꦱꦗꦮ', - ka: 'ქართული', - kk: 'Қазақша', - ko: '한국어', - lt: 'Lietuvių', - lv: 'Latviešu', - ml: 'മലയാളം', - ms: 'Bahasa Melayu', - nl: 'Nederlands', - no: 'Norsk', - oc: 'Occitan', - pl: 'Polski', - pt: 'Português', - 'pt-BR': 'Português do Brasil', - ro: 'Română', - ru: 'Русский', - sk: 'Slovenčina', - sl: 'Slovenščina', - sq: 'Shqip', - sr: 'Српски', - 'sr-Latn': 'Srpski (latinica)', - sv: 'Svenska', - ta: 'தமிழ்', - te: 'తెలుగు', - th: 'ไทย', - tr: 'Türkçe', - uk: 'Українська', - zh: '中文', - 'zh-CN': '简体中文', - 'zh-HK': '繁體中文(香港)', - 'zh-TW': '繁體中文(臺灣)', -}; - interface IGenerateFilter { name: string; state: boolean | null; @@ -109,14 +41,8 @@ interface IGenerateFilter { const ExplorerFilter = () => { const dispatch = useAppDispatch(); - const [showReplies, setShowReplies] = useState(false); - const [inputValue, setInputValue] = useState(''); - const [include, setInclude] = useState(''); - const [isOpen, setIsOpen] = useState(false); - - const hasValue = inputValue.length > 0; - const intl = useIntl(); + const [isOpen, setIsOpen] = useState(false); const [tagFilters, setTagFilters] = useState([ { 'name': 'Nostr', state: null, 'value': 'protocol:nostr' }, @@ -124,96 +50,6 @@ const ExplorerFilter = () => { { 'name': 'Fediverse', state: null, 'value': 'protocol:activitypub' }, ]); - const handleToggleReplies: React.ChangeEventHandler = (e) => { - setShowReplies(!showReplies); - const isOn = e.target.checked; - - if (isOn) { - setTagFilters((prevValue) => [...prevValue.filter((prev) => prev.name.toLowerCase() !== 'reply'), { name: 'Reply', state: null, value: 'reply:true' }]); - } else { - setTagFilters((prevValue) => [...prevValue.filter((prev) => prev.name.toLowerCase() !== 'reply')]); - } - }; - - const toggleProtocolFilter = (protocolName: string, protocolValue: string) => { - setTagFilters(prevFilters => { - - const exists = prevFilters.some(tag => tag.name.toLowerCase() === protocolName.toLowerCase() && tag.value[0] !== '-'); - const newFilterList = prevFilters.filter(tag => tag.name.toLowerCase() !== protocolName.toLowerCase()); - - const newFilter = { - name: protocolName, - state: null, - value: exists ? `-protocol:${protocolValue}` : `protocol:${protocolValue}`, - }; - - if (newFilterList.length === 0) { - return [newFilter]; - } - - return [newFilterList[0], newFilter, ...newFilterList.slice(1)]; - }); - }; - - const generateFilter = ({ name, state }: IGenerateFilter) => { - let borderColor = ''; - let textColor = ''; - switch (name.toLowerCase()) { - case 'nostr': - borderColor = 'border-purple-500'; - textColor = 'text-purple-500'; - break; - case 'bluesky': - borderColor = 'border-blue-500'; - textColor = 'text-blue-500'; - break; - case 'fediverse': - borderColor = 'border-indigo-500'; - textColor = 'text-indigo-500'; - break; - default: - if (name.toLowerCase() === 'reply' || Object.keys(languages).some((lang) => lang === name.toLowerCase())) { - borderColor = 'border-grey-500'; - textColor = 'text-grey-500'; - break; - } - borderColor = state ? 'border-green-500' : 'border-red-500'; - textColor = state ? 'text-green-500' : 'text-red-500'; - } - - return ( -
- {name} - setTagFilters((prevValue) => { - return prevValue.filter((x) => x.name !== name); - })} - /> -
- ); - }; - - const handleSelectChange: React.ChangeEventHandler = e => { - const value = e.target.value; - - if (value.toLowerCase() === 'default') { - setTagFilters((prevValue) => prevValue.filter((value) => !value.value.includes('language:'))); - } else { - setTagFilters((prevValue) => { - return [{ name: value.toUpperCase(), state: null, value: `language:${value}` }, ...prevValue.filter((value) => !value.value.includes('language:'))]; - }); - } - }; - - const handleAddFilter = () => { - setTagFilters((prev) => { - return [...prev, { name: inputValue, state: include === '', value: `${include}${inputValue.split(' ').join(` ${include}`)}` }]; - }); - }; - useEffect( () => { @@ -237,7 +73,7 @@ const ExplorerFilter = () => { {intl.formatMessage(messages.filters)} - {tagFilters.length > 0 && [...tagFilters.slice(0, 3).filter((x)=> x.value[0] !== '-' && x.state === null).map(generateFilter), ...tagFilters.slice(3).map(generateFilter)]} + {tagFilters.length > 0 && [...tagFilters.slice(0, 3).filter((x)=> x.value[0] !== '-' && x.state === null).map((value) => generateFilter(value, setTagFilters)), ...tagFilters.slice(3).map((value) => generateFilter(value, setTagFilters))]} { {/* Show Reply toggle */} - - - {intl.formatMessage(messages.showReplies)} - - - - - + {/* Language */} - - - {intl.formatMessage(messages.language)} - - - - + {/* Platforms */} - - - {intl.formatMessage(messages.platforms)} - - - {/* Nostr */} - - tag.name.toLowerCase() === 'nostr' && tag.value[0] !== '-')} - onChange={() => toggleProtocolFilter('Nostr', 'nostr')} - /> - - {intl.formatMessage(messages.nostr)} - - - - {/* Bluesky */} - - tag.name.toLowerCase() === 'bluesky' && tag.value[0] !== '-')} - onChange={() => toggleProtocolFilter('Bluesky', 'atproto')} - /> - - {intl.formatMessage(messages.bluesky)} - - - - {/* Fediverse */} - - tag.name.toLowerCase() === 'fediverse' && tag.value[0] !== '-')} - onChange={() => toggleProtocolFilter('Fediverse', 'activitypub')} - /> - - {intl.formatMessage(messages.fediverse)} - - - - + {/* Create your filter */} - - - {intl.formatMessage(messages.createYourFilter)} - + - - - {intl.formatMessage(messages.filterByWords)} - - - - - -
- setInputValue(e.target.value)} /> -
- - - -
- -
- - {/* Include */} - - 0)} - onChange={() => { - setInclude(''); - }} - /> - - {intl.formatMessage(messages.include)} - - - - {/* Exclude */} - - 0)} - onChange={() => { - setInclude('-'); - }} - /> - - {intl.formatMessage(messages.exclude)} - - -
-
- - - - - - - -
@@ -411,4 +107,5 @@ const ExplorerFilter = () => { }; -export default ExplorerFilter; \ No newline at end of file +export default ExplorerFilter; +export type { IGenerateFilter }; \ No newline at end of file diff --git a/src/features/search/components/filters.tsx b/src/features/search/components/filters.tsx new file mode 100644 index 000000000..efcbc9d12 --- /dev/null +++ b/src/features/search/components/filters.tsx @@ -0,0 +1,381 @@ +import searchIcon from '@tabler/icons/outline/search.svg'; +import xIcon from '@tabler/icons/outline/x.svg'; +import clsx from 'clsx'; +import { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import Checkbox from 'soapbox/components/ui/checkbox.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Input from 'soapbox/components/ui/input.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import Toggle from 'soapbox/components/ui/toggle.tsx'; +import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; +import { IGenerateFilter } from 'soapbox/features/search/components/explorerFilter.tsx'; +// import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; + +const messages = defineMessages({ + filters: { id: 'column.explorer.filters', defaultMessage: 'Filters:' }, + showReplies: { id: 'column.explorer.filters.show_replies', defaultMessage: 'Show replies:' }, + language: { id: 'column.explorer.filters.language', defaultMessage: 'Language:' }, + platforms: { id: 'column.explorer.filters.platforms', defaultMessage: 'Platforms:' }, + createYourFilter: { id: 'column.explorer.filters.create_your_filter', defaultMessage: 'Create your filter' }, + filterByWords: { id: 'column.explorer.filters.filter_by_words', defaultMessage: 'Filter by this/these words' }, + include: { id: 'column.explorer.filters.include', defaultMessage: 'Include' }, + exclude: { id: 'column.explorer.filters.exclude', defaultMessage: 'Exclude' }, + nostr: { id: 'column.explorer.filters.nostr', defaultMessage: 'Nostr' }, + bluesky: { id: 'column.explorer.filters.bluesky', defaultMessage: 'Bluesky' }, + fediverse: { id: 'column.explorer.filters.fediverse', defaultMessage: 'Fediverse' }, + cancel: { id: 'column.explorer.filters.cancel', defaultMessage: 'Cancel' }, + addFilter: { id: 'column.explorer.filters.add_filter', defaultMessage: 'Add Filter' }, +}); + +const languages = { + default: 'Global', + en: 'English', + ar: 'العربية', + ast: 'Asturianu', + bg: 'Български', + bn: 'বাংলা', + ca: 'Català', + co: 'Corsu', + cs: 'Čeština', + cy: 'Cymraeg', + da: 'Dansk', + de: 'Deutsch', + el: 'Ελληνικά', + 'en-Shaw': '𐑖𐑱𐑝𐑾𐑯', + eo: 'Esperanto', + es: 'Español', + eu: 'Euskara', + fa: 'فارسی', + fi: 'Suomi', + fr: 'Français', + ga: 'Gaeilge', + gl: 'Galego', + he: 'עברית', + hi: 'हिन्दी', + hr: 'Hrvatski', + hu: 'Magyar', + hy: 'Հայերեն', + id: 'Bahasa Indonesia', + io: 'Ido', + is: 'íslenska', + it: 'Italiano', + ja: '日本語', + jv: 'ꦧꦱꦗꦮ', + ka: 'ქართული', + kk: 'Қазақша', + ko: '한국어', + lt: 'Lietuvių', + lv: 'Latviešu', + ml: 'മലയാളം', + ms: 'Bahasa Melayu', + nl: 'Nederlands', + no: 'Norsk', + oc: 'Occitan', + pl: 'Polski', + pt: 'Português', + 'pt-BR': 'Português do Brasil', + ro: 'Română', + ru: 'Русский', + sk: 'Slovenčina', + sl: 'Slovenščina', + sq: 'Shqip', + sr: 'Српски', + 'sr-Latn': 'Srpski (latinica)', + sv: 'Svenska', + ta: 'தமிழ்', + te: 'తెలుగు', + th: 'ไทย', + tr: 'Türkçe', + uk: 'Українська', + zh: '中文', + 'zh-CN': '简体中文', + 'zh-HK': '繁體中文(香港)', + 'zh-TW': '繁體中文(臺灣)', +}; + +interface IFilter { + onChangeFilters: React.Dispatch>; +} + +interface IPlatformFilters { + filters: IGenerateFilter[]; + onChangeFilters: React.Dispatch>; +} + +const PlatformFilters = ({ onChangeFilters, filters }: IPlatformFilters) => { + const intl = useIntl(); + + const toggleProtocolFilter = (protocolName: string, protocolValue: string) => { + onChangeFilters(prevFilters => { + + const exists = prevFilters.some(tag => tag.name.toLowerCase() === protocolName.toLowerCase() && tag.value[0] !== '-'); + const newFilterList = prevFilters.filter(tag => tag.name.toLowerCase() !== protocolName.toLowerCase()); + + const newFilter = { + name: protocolName, + state: null, + value: exists ? `-protocol:${protocolValue}` : `protocol:${protocolValue}`, + }; + + if (newFilterList.length === 0) { + return [newFilter]; + } + + return [newFilterList[0], newFilter, ...newFilterList.slice(1)]; + }); + }; + + return ( + + + {intl.formatMessage(messages.platforms)} + + + {/* Nostr */} + + tag.name.toLowerCase() === 'nostr' && tag.value[0] !== '-')} + onChange={() => toggleProtocolFilter('Nostr', 'nostr')} + /> + + {intl.formatMessage(messages.nostr)} + + + + {/* Bluesky */} + + tag.name.toLowerCase() === 'bluesky' && tag.value[0] !== '-')} + onChange={() => toggleProtocolFilter('Bluesky', 'atproto')} + /> + + {intl.formatMessage(messages.bluesky)} + + + + {/* Fediverse */} + + tag.name.toLowerCase() === 'fediverse' && tag.value[0] !== '-')} + onChange={() => toggleProtocolFilter('Fediverse', 'activitypub')} + /> + + {intl.formatMessage(messages.fediverse)} + + + + + ); + +}; + +const CreateFilter = ({ onChangeFilters }: IFilter) => { + const intl = useIntl(); + const [inputValue, setInputValue] = useState(''); + const [include, setInclude] = useState(''); + const hasValue = inputValue.length > 0; + + const handleAddFilter = () => { + onChangeFilters((prev) => { + return [...prev, { name: inputValue, state: include === '', value: `${include}${inputValue.split(' ').join(` ${include}`)}` }]; + }); + }; + + return ( + + + {intl.formatMessage(messages.createYourFilter)} + + + + + {intl.formatMessage(messages.filterByWords)} + + + + + +
+ setInputValue(e.target.value)} /> +
+ + + setInputValue('')} + className={clsx('size-4 text-gray-600', { hidden: !hasValue })} + /> +
+ +
+ + {/* Include */} + + 0)} + onChange={() => { + setInclude(''); + }} + /> + + {intl.formatMessage(messages.include)} + + + + {/* Exclude */} + + 0)} + onChange={() => { + setInclude('-'); + }} + /> + + {intl.formatMessage(messages.exclude)} + + +
+
+ + + + + + + +
+ ); + +}; + +const LanguageFilter = ({ onChangeFilters }: IFilter) => { + const intl = useIntl(); + + const handleSelectChange: React.ChangeEventHandler = e => { + const value = e.target.value; + + if (value.toLowerCase() === 'default') { + onChangeFilters((prevValue) => prevValue.filter((value) => !value.value.includes('language:'))); + } else { + onChangeFilters((prevValue) => { + return [{ name: value.toUpperCase(), state: null, value: `language:${value}` }, ...prevValue.filter((value) => !value.value.includes('language:'))]; + }); + } + }; + + return ( + + + {intl.formatMessage(messages.language)} + + + + + ); + +}; + +const RepliesFilter = ({ onChangeFilters }: IFilter) => { + const intl = useIntl(); + const [showReplies, setShowReplies] = useState(false); + + const handleToggleReplies: React.ChangeEventHandler = (e) => { + setShowReplies(!showReplies); + const isOn = e.target.checked; + + if (isOn) { + onChangeFilters((prevValue) => [...prevValue.filter((prev) => prev.name.toLowerCase() !== 'reply'), { name: 'Reply', state: null, value: 'reply:true' }]); + } else { + onChangeFilters((prevValue) => [...prevValue.filter((prev) => prev.name.toLowerCase() !== 'reply')]); + } + }; + + return ( + + + {intl.formatMessage(messages.showReplies)} + + + + ); + +}; + +const generateFilter = ({ name, state }: IGenerateFilter, onChangeFilters: React.Dispatch>) => { + let borderColor = ''; + let textColor = ''; + switch (name.toLowerCase()) { + case 'nostr': + borderColor = 'border-purple-500'; + textColor = 'text-purple-500'; + break; + case 'bluesky': + borderColor = 'border-blue-500'; + textColor = 'text-blue-500'; + break; + case 'fediverse': + borderColor = 'border-indigo-500'; + textColor = 'text-indigo-500'; + break; + default: + if (name.toLowerCase() === 'reply' || Object.keys(languages).some((lang) => lang === name.toLowerCase())) { + borderColor = 'border-grey-500'; + textColor = 'text-grey-500'; + break; + } + borderColor = state ? 'border-green-500' : 'border-red-500'; + textColor = state ? 'text-green-500' : 'text-red-500'; + } + + return ( +
+ {name} + onChangeFilters((prevValue) => { + return prevValue.filter((x) => x.name !== name); + })} + /> +
+ ); +}; + +export { CreateFilter, PlatformFilters, LanguageFilter, RepliesFilter, generateFilter }; \ No newline at end of file