diff --git a/packages/common/src/services/remote-config/feature-flags.ts b/packages/common/src/services/remote-config/feature-flags.ts index bbc4c73be6f..f3b4be92ac1 100644 --- a/packages/common/src/services/remote-config/feature-flags.ts +++ b/packages/common/src/services/remote-config/feature-flags.ts @@ -63,6 +63,7 @@ export enum FeatureFlags { DISCOVERY_TIP_REACTIONS = 'discovery_tip_reactions', USE_ADDRESS_LOOKUPS = 'use_address_lookups', MANAGER_MODE = 'manager_mode', + SEARCH_V2 = 'search_v2', USE_SDK_PURCHASE_TRACK = 'use_sdk_purchase_track', USE_SDK_PURCHASE_ALBUM = 'use_sdk_purchase_album' } @@ -143,6 +144,7 @@ export const flagDefaults: FlagDefaults = { [FeatureFlags.DISCOVERY_TIP_REACTIONS]: false, [FeatureFlags.USE_ADDRESS_LOOKUPS]: false, [FeatureFlags.MANAGER_MODE]: false, + [FeatureFlags.SEARCH_V2]: false, [FeatureFlags.USE_SDK_PURCHASE_TRACK]: false, [FeatureFlags.USE_SDK_PURCHASE_ALBUM]: false } diff --git a/packages/harmony/src/components/index.ts b/packages/harmony/src/components/index.ts index 2ad33e52324..9b970e8b4f0 100644 --- a/packages/harmony/src/components/index.ts +++ b/packages/harmony/src/components/index.ts @@ -19,4 +19,5 @@ export * from './progress-bar' export * from './scrubber' export * from './skeleton' export * from './artwork' +export { default as LoadingSpinner } from './loading-spinner/LoadingSpinner' export * from './pill' diff --git a/packages/web/src/app/AppProviders.tsx b/packages/web/src/app/AppProviders.tsx index 1ff985cb7f9..fbae7399655 100644 --- a/packages/web/src/app/AppProviders.tsx +++ b/packages/web/src/app/AppProviders.tsx @@ -6,7 +6,7 @@ import { LastLocationProvider } from 'react-router-last-location' import { RouterContextProvider } from 'components/animated-switch/RouterContextProvider' import { HeaderContextProvider } from 'components/header/mobile/HeaderContextProvider' -import { NavProvider } from 'components/nav/store/context' +import { NavProvider } from 'components/nav/mobile/NavContext' import { ScrollProvider } from 'components/scroll-provider/ScrollProvider' import { ToastContextProvider } from 'components/toast/ToastContext' import { getSystemAppearance, getTheme } from 'utils/theme/theme' diff --git a/packages/web/src/app/web-player/WebPlayer.jsx b/packages/web/src/app/web-player/WebPlayer.jsx index 9978a715349..b4668e97a57 100644 --- a/packages/web/src/app/web-player/WebPlayer.jsx +++ b/packages/web/src/app/web-player/WebPlayer.jsx @@ -81,6 +81,7 @@ import RepostsPage from 'pages/reposts-page/RepostsPage' import { RequiresUpdate } from 'pages/requires-update/RequiresUpdate' import SavedPage from 'pages/saved-page/SavedPage' import SearchPage from 'pages/search-page/SearchPage' +import { SearchPageV2 } from 'pages/search-page-v2/SearchPageV2' import SettingsPage from 'pages/settings-page/SettingsPage' import { SubPage } from 'pages/settings-page/components/mobile/SettingsPage' import SmartCollectionPage from 'pages/smart-collection/SmartCollectionPage' @@ -398,7 +399,8 @@ class WebPlayer extends Component { } render() { - const { incrementScroll, decrementScroll, userHandle } = this.props + const { incrementScroll, decrementScroll, userHandle, isSearchV2Enabled } = + this.props const { showWebUpdateBanner, @@ -647,23 +649,31 @@ class WebPlayer extends Component { ( - - )} + render={(props) => + isSearchV2Enabled ? ( + + ) : ( + + ) + } /> ( - - )} + render={(props) => + isSearchV2Enabled ? ( + + ) : ( + + ) + } /> ({ accountStatus: getAccountStatus(state), signOnStatus: getSignOnStatus(state), showCookieBanner: getShowCookieBanner(state), - isChatEnabled: getFeatureEnabled(FeatureFlags.CHAT_ENABLED) + isChatEnabled: getFeatureEnabled(FeatureFlags.CHAT_ENABLED), + isSearchV2Enabled: getFeatureEnabled(FeatureFlags.SEARCH_V2) }) const mapDispatchToProps = (dispatch) => ({ diff --git a/packages/web/src/components/add-to-collection/mobile/AddToCollection.tsx b/packages/web/src/components/add-to-collection/mobile/AddToCollection.tsx index b4b426b7723..3c328d2b91c 100644 --- a/packages/web/src/components/add-to-collection/mobile/AddToCollection.tsx +++ b/packages/web/src/components/add-to-collection/mobile/AddToCollection.tsx @@ -14,8 +14,8 @@ import { Dispatch } from 'redux' import { CollectionCard } from 'components/collection' import CardLineup from 'components/lineup/CardLineup' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' +import { useTemporaryNavContext } from 'components/nav/mobile/NavContext' import TextElement, { Type } from 'components/nav/mobile/TextElement' -import { useTemporaryNavContext } from 'components/nav/store/context' import { ToastContext } from 'components/toast/ToastContext' import useHasChangedRoute from 'hooks/useHasChangedRoute' import NewCollectionButton from 'pages/saved-page/components/mobile/NewCollectionButton' diff --git a/packages/web/src/components/edit-playlist/mobile/EditPlaylistPage.tsx b/packages/web/src/components/edit-playlist/mobile/EditPlaylistPage.tsx index 22a69e5af4f..4e1eae8e70f 100644 --- a/packages/web/src/components/edit-playlist/mobile/EditPlaylistPage.tsx +++ b/packages/web/src/components/edit-playlist/mobile/EditPlaylistPage.tsx @@ -23,8 +23,8 @@ import DynamicImage from 'components/dynamic-image/DynamicImage' import EditableRow, { Format } from 'components/groupable-list/EditableRow' import GroupableList from 'components/groupable-list/GroupableList' import Grouping from 'components/groupable-list/Grouping' +import { useTemporaryNavContext } from 'components/nav/mobile/NavContext' import TextElement, { Type } from 'components/nav/mobile/TextElement' -import { useTemporaryNavContext } from 'components/nav/store/context' import TrackList from 'components/track/mobile/TrackList' import { useCollectionCoverArt } from 'hooks/useCollectionCoverArt' import useHasChangedRoute from 'hooks/useHasChangedRoute' diff --git a/packages/web/src/components/header/desktop/Header.jsx b/packages/web/src/components/header/desktop/Header.jsx index 0b16ff7d805..0ec7ff16830 100644 --- a/packages/web/src/components/header/desktop/Header.jsx +++ b/packages/web/src/components/header/desktop/Header.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { BackButton } from 'components/back-button/BackButton' import { HeaderGutter } from 'components/header/desktop/HeaderGutter' +import { useMedia } from 'hooks/useMedia' import styles from './Header.module.css' @@ -32,6 +33,8 @@ const Header = (props) => { [styles.page]: variant === 'page' } + const { isMobile } = useMedia() + return ( <> { scrollBarWidth={scrollBarWidth} />
{ + const { history } = useHistoryContext() const { leftElement, centerElement, rightElement } = useContext(NavContext)! + const { isEnabled: isSearchV2Enabled } = useFeatureFlag( + FeatureFlags.SEARCH_V2 + ) const [isSearching, setIsSearching] = useState(false) const [searchValue, setSearchValue] = useState('') @@ -84,11 +91,17 @@ const NavBar = ({ useEffect(() => { const splitPath = pathname.split('/') const isSearch = splitPath.length > 1 && splitPath[1] === 'search' - if (!isSearch) { - setIsSearching(false) - } + setIsSearching(isSearch) }, [pathname]) + const handleOpenSearch = useCallback(() => { + if (isSearchV2Enabled) { + history.push(`/search`) + } else { + setIsSearching(true) + } + }, [history, isSearchV2Enabled]) + const onCloseSearch = () => { setIsSearching(false) setSearchValue('') @@ -243,13 +256,15 @@ const NavBar = ({ {rightElement === RightPreset.SEARCH ? ( { - setIsSearching(true) - }} + onOpen={handleOpenSearch} onClose={onCloseSearch} value={searchValue} onSearch={setSearchValue} - placeholder={messages.searchPlaceholder} + placeholder={ + isSearchV2Enabled + ? messages.searchPlaceholderV2 + : messages.searchPlaceholder + } showHeader={false} className={cn( styles.searchBar, diff --git a/packages/web/src/components/nav/store/context.tsx b/packages/web/src/components/nav/mobile/NavContext.tsx similarity index 100% rename from packages/web/src/components/nav/store/context.tsx rename to packages/web/src/components/nav/mobile/NavContext.tsx diff --git a/packages/web/src/components/notification/NotificationPage.tsx b/packages/web/src/components/notification/NotificationPage.tsx index 329728d437c..aa8b87b9439 100644 --- a/packages/web/src/components/notification/NotificationPage.tsx +++ b/packages/web/src/components/notification/NotificationPage.tsx @@ -12,7 +12,7 @@ import { useDispatch, useSelector } from 'react-redux' import loadingSpinner from 'assets/animations/loadingSpinner.json' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import { EmptyNotifications } from './EmptyNotifications' import { Notification } from './Notification' diff --git a/packages/web/src/components/search-bar/ConnectedSearchBar.jsx b/packages/web/src/components/search-bar/ConnectedSearchBar.jsx index c726932f8d3..602d7b142a2 100644 --- a/packages/web/src/components/search-bar/ConnectedSearchBar.jsx +++ b/packages/web/src/components/search-bar/ConnectedSearchBar.jsx @@ -5,6 +5,7 @@ import { imageProfilePicEmpty as profilePicEmpty } from '@audius/common/assets' import { Name, SquareSizes } from '@audius/common/models' +import { FeatureFlags } from '@audius/common/services' import { getTierForUser } from '@audius/common/store' import { push as pushRoute } from 'connected-react-router' import { has } from 'lodash' @@ -20,7 +21,9 @@ import { clearSearch } from 'common/store/search-bar/actions' import { getSearch } from 'common/store/search-bar/selectors' -import Bar from 'components/search/SearchBar' +import SearchBar from 'components/search/SearchBar' +import SearchBarV2 from 'components/search/SearchBarV2' +import { getFeatureEnabled } from 'services/remote-config/featureFlagHelpers' import { collectionPage, profilePage, getPathname } from 'utils/route' import styles from './ConnectedSearchBar.module.css' @@ -256,9 +259,12 @@ class ConnectedSearchBar extends Component { 0 ) const { status, searchText } = this.props.search + const SearchBarComponent = this.props.isSearchV2Enabled + ? SearchBarV2 + : SearchBar return (
- ({ - search: getSearch(state, props) + search: getSearch(state, props), + isSearchV2Enabled: getFeatureEnabled(FeatureFlags.SEARCH_V2) }) const mapDispatchToProps = (dispatch) => ({ fetchSearch: (value) => dispatch(fetchSearch(value)), diff --git a/packages/web/src/components/search/SearchBarV2.jsx b/packages/web/src/components/search/SearchBarV2.jsx new file mode 100644 index 00000000000..2464a60e32d --- /dev/null +++ b/packages/web/src/components/search/SearchBarV2.jsx @@ -0,0 +1,465 @@ +import { createRef, Component } from 'react' + +import { Kind, Status } from '@audius/common/models' +import { + IconArrowRight as IconArrow, + IconSearch, + setupHotkeys, + removeHotkeys +} from '@audius/harmony' +import AutoComplete from 'antd/lib/auto-complete' +import Input from 'antd/lib/input' +import cn from 'classnames' +import { isEqual } from 'lodash' +import PropTypes from 'prop-types' +import Lottie from 'react-lottie' +// eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web +import { Transition } from 'react-spring/renderprops.cjs' + +import loadingSpinner from 'assets/animations/loadingSpinner.json' +import SearchBarResult from 'components/search/SearchBarResult' + +import styles from './SearchBar.module.css' + +const SEARCH_BAR_OPTION = 'SEARCH_BAR_OPTION' +const ALL_RESULTS_OPTION = 'ALL_RESULTS_OPTION' +const NO_RESULTS_OPTION = 'NO_RESULTS_OPTION' + +const messages = { + searchTagsTitle: (tag) => `Search Tags for ${tag}`, + searchTagsDisabled: () => 'Search Tags' +} + +const maxLength = 500 + +const TagSearchPopup = ({ tag, style, onClick, disabled, focused }) => ( +
!disabled && onClick()} + > +
+ {messages[disabled ? 'searchTagsDisabled' : 'searchTagsTitle'](tag)} + {!disabled && } +
+
+) + +class SearchBar extends Component { + constructor(props) { + super(props) + this.state = { + open: false, + focused: false, + // State variable set to true when an item has been selected. + selected: false, + value: '', + // Indicates whether we are receiving a value prop from the parent + // that is different than the value state in this class. + // For example, when the user visits /search/, this + // component receives an initial prop value = but we do not + // want to trigger an animation to the open state + // (though we do want to collect search results). + valueFromParent: false, + // Debounce used to delay search for a small amount of time to avoid + // too many search calls. + debounce: null, + openAnimationTimeout: null, + hotkeyHook: null, + shouldDismissTagPopup: false, + tagPopupFocused: false + } + this.autoCompleteRef = createRef() + this.searchBarRef = createRef() + } + + componentDidMount() { + const hook = setupHotkeys({ + 191 /* slash */: () => { + this.autoCompleteRef.current.focus() + } + }) + this.setState({ hotkeyHook: hook }) + } + + componentWillUnmount() { + removeHotkeys(this.state.hotkeyHook) + this.setState({ hotkeyHook: null }) + } + + onSearch = (value, action) => { + const trimmedValue = value.slice(0, maxLength) + this.setState({ value: trimmedValue, valueFromParent: false }) + + // Set the search state but don't actually call search + this.props.onSearch(trimmedValue, false) + // Set a debounce timer for 100ms to actually send the search + this.setState({ + debounce: setTimeout(() => { + this.props.onSearch(trimmedValue, true) + }, 100) + }) + } + + onChange = (value) => { + clearTimeout(this.state.debounce) + } + + onSelect = (value, option) => { + if (value === SEARCH_BAR_OPTION || value === ALL_RESULTS_OPTION) { + // Disallow empty searches. + if (this.state.value !== '') { + this.props.onSubmit(this.state.value) + } + } else if (value !== NO_RESULTS_OPTION) { + this.props.goToRoute(value) + if (this.props.onSelect) this.props.onSelect(value) + } + // Lose focus on the bar, timeout the blur so it pops to the end of the event loop. + setTimeout(() => { + this.autoCompleteRef.current && this.autoCompleteRef.current.blur() + }, 0) + this.setState({ selected: true }) + this.onBlur() + } + + onFocus = () => { + this.setState({ shouldDismissTagPopup: false, focused: true }) + this.searchBarRef.current + .getElementsByClassName('ant-select-selection-search')[0] + .classList.add('expanded') + if (this.state.value !== '') { + // Delay search results open animation while the search bar expands. + this.setState({ + openAnimationTimeout: setTimeout(() => { + this.onSearch(this.state.value) + this.setState({ open: true, selected: false }) + }, 200) + }) + } + } + + onBlur = () => { + if (document.hasFocus()) { + this.props.onCancel() + // Clear the open animation timeout just in case the user suddenly loses focus on the + // search bar while an animation to open is happening. + clearTimeout(this.state.openAnimationTimeout) + if (this.state.open) { + // Delay search bar collapse while search results close. + setTimeout(() => { + this.searchBarRef.current && + this.searchBarRef.current + .getElementsByClassName('ant-select-selection-search')[0] + .classList.remove('expanded') + }, 200) + } else { + this.searchBarRef.current + .getElementsByClassName('ant-select-selection-search')[0] + .classList.remove('expanded') + } + this.setState({ open: false, focused: false }) + } + } + + onKeyDown = (e) => { + // Stop up arrow and down arrow from moving the cursor in the text input. + switch (e.keyCode) { + case 38 /* up */: + e.preventDefault() + this.setState({ tagPopupFocused: false }) + break + case 40 /* down */: + e.preventDefault() + this.setState({ tagPopupFocused: true }) + break + case 27 /* esc */: + this.setState({ tagPopupFocused: false }) + this.autoCompleteRef.current.blur() + this.onBlur() + break + case 13 /* enter */: + this.autoCompleteRef.current.blur() + this.setState({ tagPopupFocused: false }) + if (this.props.isTagSearch) + this.setState({ shouldDismissTagPopup: true }) + if ( + (this.props.isTagSearch && this.state.value.length > 1) || + !this.props.isTagSearch + ) { + this.props.onSubmit(this.state.value) + this.setState({ debounce: null }) + } + break + default: + } + } + + renderTitle(title) { + return ( + + {title} +
+ + ) + } + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.value !== prevState.value) { + return { value: nextProps.value, valueFromParent: true } + } else return null + } + + shouldComponentUpdate(nextProps, nextState) { + // Don't rerender right away if there is a new data source because rerendering + // immediately would throw away the opening animation. + if ( + !isEqual(nextProps.dataSource, this.props.dataSource) && + nextProps.resultsCount > 0 + ) { + if ( + !nextState.selected && + !nextState.valueFromParent && + // Only open the suggestions if the search bar is focused + nextState.focused + ) { + this.setState({ open: true }) + } + if (nextProps.status === Status.SUCCESS) { + return true + } + return false + } + if ( + this.props.status === Status.LOADING && + nextProps.status === Status.SUCCESS && + nextProps.resultsCount === 0 && + this.state.value !== '' + ) { + if ( + !nextState.selected && + !nextState.valueFromParent && + // Only open the suggestions if the search bar is focused + nextState.focused + ) { + this.setState({ open: true }) + } + return false + } + // Close the dropdown if we're searching for '' (deleted text in search). + if (nextState.value === '' && this.state.value !== nextState.value) { + this.setState({ open: false }) + return false + } + // Make sure that we clear the 'selected' bit so that we can process + // it again in the next update. + if (this.state.selected && this.state.valueFromParent) { + this.setState({ selected: false }) + } + return true + } + + render() { + const { status, searchText, dataSource, resultsCount, isTagSearch } = + this.props + const searchResults = dataSource.sections + .filter((group) => { + if (group.children.length < 1) { + return false + } + const vals = group.children + .slice(0, Math.min(3, group.children.length)) + .filter((opt) => { + return opt.key || opt.primary + }) + if (vals < 1) { + return false + } + return true + }) + .map((group) => ({ + label: this.renderTitle(group.title), + options: group.children + .slice(0, Math.min(3, group.children.length)) + .map((opt) => ({ + label: ( +
+ +
+ ), + value: (opt.key || opt.primary).toString() + })) + })) + + let options = [] + if (resultsCount > 0) { + const allResultsOption = { + label: ( +
+
+
+ View More Results + +
+
+
+ ), + value: ALL_RESULTS_OPTION + } + options = searchResults.concat(allResultsOption) + } else { + if (status !== Status.LOADING && searchText !== '') { + // Need to ensure searchText !== '' in this case, + // because we clear the search if we lose focus, + // causing status to be set to SUCCESS, + // but we don't want to show the no results box. + + const noResultOption = { + label: ( +
+
+ No Results +
+
+ ), + value: NO_RESULTS_OPTION + } + + options = [noResultOption] + } + } + + // If we're searching for a tag, + // don't open the autocomplete popup, and instead show our + // own custom component below. + const showAutocomplete = !isTagSearch && this.state.open + const showTagPopup = + isTagSearch && this.state.open && !this.state.shouldDismissTagPopup + return ( +
+ {/* show search spinner if not a tag search and there is some value present */} + {!isTagSearch && this.state.value && ( +
+ +
+ )} + trigger.parentNode} + > + this.props.onSubmit('')} + /> + } + onKeyDown={this.onKeyDown} + spellCheck={false} + /> + + + {(item) => + item && + ((props) => ( + { + this.props.onSubmit(this.state.value) + }} + disabled={this.state.value.length < 2} // Don't allow clicks until we've typed our first letter past '#' + focused={this.state.tagPopupFocused} + /> + )) + } + +
+ ) + } +} + +SearchBar.propTypes = { + dataSource: PropTypes.object, + resultsCount: PropTypes.number, + onSearch: PropTypes.func, + onCancel: PropTypes.func, + onSubmit: PropTypes.func, + goToRoute: PropTypes.func +} + +SearchBar.defaultProps = { + dataSource: [], + resultsCount: 0 +} + +export default SearchBar diff --git a/packages/web/src/components/trending-genre-selection/components/TrendingGenreSelectionPage.tsx b/packages/web/src/components/trending-genre-selection/components/TrendingGenreSelectionPage.tsx index 30f5a3bae2b..638c643f32d 100644 --- a/packages/web/src/components/trending-genre-selection/components/TrendingGenreSelectionPage.tsx +++ b/packages/web/src/components/trending-genre-selection/components/TrendingGenreSelectionPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useContext } from 'react' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import GenreSelectionList from 'pages/trending-page/components/GenreSelectionList' import styles from './TrendingGenreSelectionPage.module.css' diff --git a/packages/web/src/pages/ai-attributed-tracks-page/components/mobile/AiPage.tsx b/packages/web/src/pages/ai-attributed-tracks-page/components/mobile/AiPage.tsx index 834fae7ba23..938fdff2afe 100644 --- a/packages/web/src/pages/ai-attributed-tracks-page/components/mobile/AiPage.tsx +++ b/packages/web/src/pages/ai-attributed-tracks-page/components/mobile/AiPage.tsx @@ -8,7 +8,7 @@ import Header from 'components/header/mobile/Header' import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import Lineup, { LineupWithoutTile } from 'components/lineup/Lineup' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useSubPageHeader } from 'components/nav/store/context' +import { useSubPageHeader } from 'components/nav/mobile/NavContext' import UserBadges from 'components/user-badges/UserBadges' import { fullAiPage } from 'utils/route' import { isMatrix } from 'utils/theme/theme' diff --git a/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx b/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx index 387122556de..83a31e81dae 100644 --- a/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx +++ b/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx @@ -10,7 +10,7 @@ import MobilePageContainer from 'components/mobile-page-container/MobilePageCont import NavContext, { LeftPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import Page from 'components/page/Page' import { useIsMobile } from 'hooks/useIsMobile' import { useFlag, useRemoteVar } from 'hooks/useRemoteConfig' diff --git a/packages/web/src/pages/collection-page/components/mobile/CollectionPage.tsx b/packages/web/src/pages/collection-page/components/mobile/CollectionPage.tsx index 1ae838dfd1d..2a8d8f9def7 100644 --- a/packages/web/src/pages/collection-page/components/mobile/CollectionPage.tsx +++ b/packages/web/src/pages/collection-page/components/mobile/CollectionPage.tsx @@ -25,7 +25,7 @@ import NavContext, { LeftPreset, CenterPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import TrackList from 'components/track/mobile/TrackList' import { smartCollectionIcons } from 'pages/collection-page/smartCollectionIcons' import { computeCollectionMetadataProps } from 'pages/collection-page/store/utils' diff --git a/packages/web/src/pages/deactivate-account-page/components/mobile/DeactivateAccountPage.tsx b/packages/web/src/pages/deactivate-account-page/components/mobile/DeactivateAccountPage.tsx index 29e7ba5435d..e036cb17dd9 100644 --- a/packages/web/src/pages/deactivate-account-page/components/mobile/DeactivateAccountPage.tsx +++ b/packages/web/src/pages/deactivate-account-page/components/mobile/DeactivateAccountPage.tsx @@ -6,7 +6,7 @@ import MobilePageContainer from 'components/mobile-page-container/MobilePageCont import NavContext, { LeftPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import { BASE_URL, DEACTIVATE_PAGE } from 'utils/route' import { diff --git a/packages/web/src/pages/explore-page/components/mobile/CollectionsPage.tsx b/packages/web/src/pages/explore-page/components/mobile/CollectionsPage.tsx index d85ca717d59..973ef883735 100644 --- a/packages/web/src/pages/explore-page/components/mobile/CollectionsPage.tsx +++ b/packages/web/src/pages/explore-page/components/mobile/CollectionsPage.tsx @@ -8,7 +8,7 @@ import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import CardLineup from 'components/lineup/CardLineup' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useSubPageHeader } from 'components/nav/store/context' +import { useSubPageHeader } from 'components/nav/mobile/NavContext' import { BASE_URL, EXPLORE_PAGE } from 'utils/route' import styles from './CollectionsPage.module.css' diff --git a/packages/web/src/pages/explore-page/components/mobile/ExplorePage.tsx b/packages/web/src/pages/explore-page/components/mobile/ExplorePage.tsx index b8bf4c30b89..4b11f3bf5de 100644 --- a/packages/web/src/pages/explore-page/components/mobile/ExplorePage.tsx +++ b/packages/web/src/pages/explore-page/components/mobile/ExplorePage.tsx @@ -30,7 +30,7 @@ import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import CardLineup from 'components/lineup/CardLineup' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useMainPageHeader } from 'components/nav/store/context' +import { useMainPageHeader } from 'components/nav/mobile/NavContext' import { UserCard } from 'components/user-card' import { useIsUSDCEnabled } from 'hooks/useIsUSDCEnabled' import useTabs from 'hooks/useTabs/useTabs' diff --git a/packages/web/src/pages/favorites-page/FavoritesPage.tsx b/packages/web/src/pages/favorites-page/FavoritesPage.tsx index 2aa3460e52b..7f84fc37d8f 100644 --- a/packages/web/src/pages/favorites-page/FavoritesPage.tsx +++ b/packages/web/src/pages/favorites-page/FavoritesPage.tsx @@ -6,7 +6,7 @@ import { } from '@audius/common/store' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' const { getUserList } = favoritesUserListSelectors diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx b/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx index d2cbb7b7bb0..d9415eb8d99 100644 --- a/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx +++ b/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx @@ -10,7 +10,7 @@ import Header from 'components/header/mobile/Header' import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import Lineup from 'components/lineup/Lineup' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useMainPageHeader } from 'components/nav/store/context' +import { useMainPageHeader } from 'components/nav/mobile/NavContext' import { FeedPageContentProps } from 'pages/feed-page/types' import { BASE_URL, FEED_PAGE } from 'utils/route' diff --git a/packages/web/src/pages/followers-page/FollowersPage.tsx b/packages/web/src/pages/followers-page/FollowersPage.tsx index c63ec18d8d4..c6d47445ac1 100644 --- a/packages/web/src/pages/followers-page/FollowersPage.tsx +++ b/packages/web/src/pages/followers-page/FollowersPage.tsx @@ -6,7 +6,7 @@ import { } from '@audius/common/store' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' const { getUserList } = followersUserListSelectors diff --git a/packages/web/src/pages/following-page/FollowingPage.tsx b/packages/web/src/pages/following-page/FollowingPage.tsx index 1981e3c6030..a081e13bfe7 100644 --- a/packages/web/src/pages/following-page/FollowingPage.tsx +++ b/packages/web/src/pages/following-page/FollowingPage.tsx @@ -6,7 +6,7 @@ import { } from '@audius/common/store' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' const { getUserList } = followingUserListSelectors diff --git a/packages/web/src/pages/history-page/components/mobile/HistoryPage.tsx b/packages/web/src/pages/history-page/components/mobile/HistoryPage.tsx index 65c81b982b7..6092c6480be 100644 --- a/packages/web/src/pages/history-page/components/mobile/HistoryPage.tsx +++ b/packages/web/src/pages/history-page/components/mobile/HistoryPage.tsx @@ -6,7 +6,7 @@ import { Button } from '@audius/harmony' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import TrackList from 'components/track/mobile/TrackList' import { TrackItemAction } from 'components/track/mobile/TrackListItem' import { TRENDING_PAGE } from 'utils/route' diff --git a/packages/web/src/pages/not-found-page/NotFoundPage.tsx b/packages/web/src/pages/not-found-page/NotFoundPage.tsx index 502785d677f..a3fd68bf7d0 100644 --- a/packages/web/src/pages/not-found-page/NotFoundPage.tsx +++ b/packages/web/src/pages/not-found-page/NotFoundPage.tsx @@ -13,7 +13,7 @@ import { useRecord, make } from 'common/store/analytics/actions' import NavContext, { CenterPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import Page from 'components/page/Page' import { useIsMobile } from 'hooks/useIsMobile' import { HOME_PAGE } from 'utils/route' diff --git a/packages/web/src/pages/notification-users-page/NotificationUsersPage.tsx b/packages/web/src/pages/notification-users-page/NotificationUsersPage.tsx index 65adf3d267c..d2efc0fc622 100644 --- a/packages/web/src/pages/notification-users-page/NotificationUsersPage.tsx +++ b/packages/web/src/pages/notification-users-page/NotificationUsersPage.tsx @@ -10,7 +10,7 @@ import { withRouter, RouteComponentProps } from 'react-router-dom' import { Dispatch } from 'redux' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' import { AppState } from 'store/types' const { getPageTitle, getUserList } = notificationsUserListSelectors diff --git a/packages/web/src/pages/pay-and-earn-page/mobile/PayAndEarnPage.tsx b/packages/web/src/pages/pay-and-earn-page/mobile/PayAndEarnPage.tsx index a722ccf24c8..fa855d064f3 100644 --- a/packages/web/src/pages/pay-and-earn-page/mobile/PayAndEarnPage.tsx +++ b/packages/web/src/pages/pay-and-earn-page/mobile/PayAndEarnPage.tsx @@ -9,7 +9,7 @@ import Header from 'components/header/mobile/Header' import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useSubPageHeader } from 'components/nav/store/context' +import { useSubPageHeader } from 'components/nav/mobile/NavContext' import { PURCHASES_PAGE, SALES_PAGE, WITHDRAWALS_PAGE } from 'utils/route' import styles from '../PayAndEarnPage.module.css' diff --git a/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx b/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx index 6dfb47c96f7..174e0eda8a0 100644 --- a/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx +++ b/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx @@ -33,11 +33,11 @@ import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import CardLineup from 'components/lineup/CardLineup' import Lineup from 'components/lineup/Lineup' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import TextElement, { Type } from 'components/nav/mobile/TextElement' import NavContext, { LeftPreset, CenterPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' +import TextElement, { Type } from 'components/nav/mobile/TextElement' import TierExplainerDrawer from 'components/user-badges/TierExplainerDrawer' import useTabs, { TabHeader } from 'hooks/useTabs/useTabs' import { useSsrContext } from 'ssr/SsrContext' diff --git a/packages/web/src/pages/remixes-page/components/mobile/RemixesPage.tsx b/packages/web/src/pages/remixes-page/components/mobile/RemixesPage.tsx index 1ac080acb72..2ddb3566430 100644 --- a/packages/web/src/pages/remixes-page/components/mobile/RemixesPage.tsx +++ b/packages/web/src/pages/remixes-page/components/mobile/RemixesPage.tsx @@ -9,7 +9,7 @@ import Header from 'components/header/mobile/Header' import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import Lineup, { LineupWithoutTile } from 'components/lineup/Lineup' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useSubPageHeader } from 'components/nav/store/context' +import { useSubPageHeader } from 'components/nav/mobile/NavContext' import UserBadges from 'components/user-badges/UserBadges' import { fullTrackRemixesPage } from 'utils/route' import { isMatrix } from 'utils/theme/theme' diff --git a/packages/web/src/pages/reposts-page/RepostsPage.tsx b/packages/web/src/pages/reposts-page/RepostsPage.tsx index 227af4aa6d5..65c9cf1f0b0 100644 --- a/packages/web/src/pages/reposts-page/RepostsPage.tsx +++ b/packages/web/src/pages/reposts-page/RepostsPage.tsx @@ -6,7 +6,7 @@ import { } from '@audius/common/store' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' const { getUserList } = repostsUserListSelectors diff --git a/packages/web/src/pages/saved-page/components/mobile/SavedPage.tsx b/packages/web/src/pages/saved-page/components/mobile/SavedPage.tsx index 34244fb99db..92661a88e51 100644 --- a/packages/web/src/pages/saved-page/components/mobile/SavedPage.tsx +++ b/packages/web/src/pages/saved-page/components/mobile/SavedPage.tsx @@ -36,7 +36,7 @@ import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import { InfiniteCardLineup } from 'components/lineup/InfiniteCardLineup' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import { useMainPageHeader } from 'components/nav/store/context' +import { useMainPageHeader } from 'components/nav/mobile/NavContext' import TrackList from 'components/track/mobile/TrackList' import { TrackItemAction } from 'components/track/mobile/TrackListItem' import { useGoToRoute } from 'hooks/useGoToRoute' diff --git a/packages/web/src/pages/search-page-v2/SearchCatalogTile.tsx b/packages/web/src/pages/search-page-v2/SearchCatalogTile.tsx new file mode 100644 index 00000000000..a2e88dfd2c3 --- /dev/null +++ b/packages/web/src/pages/search-page-v2/SearchCatalogTile.tsx @@ -0,0 +1,35 @@ +import { IconSearch, Paper, Text, useTheme } from '@audius/harmony' + +import { useMedia } from 'hooks/useMedia' +const messages = { + cta: 'Search the Catalog', + description: + 'Apply filters or search to discover tracks, profile, playlists, and albums.' +} + +export const SearchCatalogTile = () => { + const { color } = useTheme() + const { isMobile } = useMedia() + return ( + + + + {messages.cta} + + + {messages.description} + + + ) +} diff --git a/packages/web/src/pages/search-page-v2/SearchPageV2.tsx b/packages/web/src/pages/search-page-v2/SearchPageV2.tsx new file mode 100644 index 00000000000..24b0ad065ed --- /dev/null +++ b/packages/web/src/pages/search-page-v2/SearchPageV2.tsx @@ -0,0 +1,178 @@ +import { ChangeEvent, useCallback, useContext, useEffect } from 'react' + +import { Status } from '@audius/common/models' +import { Maybe } from '@audius/common/utils' +import { + Flex, + IconAlbum, + IconComponent, + IconNote, + IconPlaylists, + IconUser, + LoadingSpinner, + RadioGroup, + SelectablePill, + Text +} from '@audius/harmony' +import { capitalize } from 'lodash' +import { useParams } from 'react-router-dom' + +import { useHistoryContext } from 'app/HistoryProvider' +import Header from 'components/header/desktop/Header' +import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' +import NavContext, { + CenterPreset, + LeftPreset, + RightPreset +} from 'components/nav/mobile/NavContext' +import Page from 'components/page/Page' +import { useMedia } from 'hooks/useMedia' + +import { SearchCatalogTile } from './SearchCatalogTile' + +type Filter = + | 'genre' + | 'mood' + | 'key' + | 'bpm' + | 'isPremium' + | 'hasDownloads' + | 'isVerified' + +type Category = { + filters: Filter[] + icon?: IconComponent +} + +const categories = { + all: { filters: [] }, + profiles: { icon: IconUser, filters: ['genre', 'isVerified'] }, + tracks: { + icon: IconNote, + filters: ['genre', 'mood', 'key', 'bpm', 'isPremium', 'hasDownloads'] + }, + albums: { icon: IconAlbum, filters: ['genre', 'mood'] }, + playlists: { icon: IconPlaylists, filters: ['genre', 'mood'] } +} satisfies Record + +type CategoryKey = keyof typeof categories + +type SearchHeaderProps = { + category?: CategoryKey + setCategory: (category: CategoryKey) => void + title: string + query: Maybe +} + +const SearchHeader = (props: SearchHeaderProps) => { + const { category: categoryKey = 'all', setCategory, query, title } = props + + const { isMobile } = useMedia() + + const handleChange = useCallback( + (e: ChangeEvent) => { + const value = e.target.value + setCategory(value as CategoryKey) + }, + [setCategory] + ) + + const categoryRadioGroup = ( + + {Object.entries(categories) + .filter(([key]) => !isMobile || key !== 'all') + .map(([key, category]) => ( + + ))} + + ) + + return isMobile ? ( + + {categoryRadioGroup} + + ) : ( +
+ + “{query}” + + + ) : null + } + rightDecorator={categoryRadioGroup} + variant='main' + /> + ) +} + +export const SearchPageV2 = () => { + const { isMobile } = useMedia() + + const { category, query } = useParams<{ + category: CategoryKey + query: string + }>() + const { history } = useHistoryContext() + + // Set nav header + const { setLeft, setCenter, setRight } = useContext(NavContext)! + useEffect(() => { + setLeft(LeftPreset.BACK) + setCenter(CenterPreset.LOGO) + setRight(RightPreset.SEARCH) + }, [setLeft, setCenter, setRight]) + + const setCategory = useCallback( + (category: CategoryKey) => { + history.push(`/search/${query}/${category}`) + }, + [history, query] + ) + + const header = ( + + ) + + const PageComponent = isMobile ? MobilePageContainer : Page + + return ( + + + {isMobile ? header : null} + {!query ? : null} + {status === Status.LOADING ? :
} +
+
+ ) +} diff --git a/packages/web/src/pages/search-page/components/mobile/SearchPageContent.tsx b/packages/web/src/pages/search-page/components/mobile/SearchPageContent.tsx index c5bb97d193d..739d9fc932d 100644 --- a/packages/web/src/pages/search-page/components/mobile/SearchPageContent.tsx +++ b/packages/web/src/pages/search-page/components/mobile/SearchPageContent.tsx @@ -33,7 +33,7 @@ import NavContext, { LeftPreset, CenterPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import { UserCard } from 'components/user-card' import useTabs from 'hooks/useTabs/useTabs' import { getCategory } from 'pages/search-page/helpers' diff --git a/packages/web/src/pages/server-track-page/mobile/ServerTrackPage.tsx b/packages/web/src/pages/server-track-page/mobile/ServerTrackPage.tsx index f25ac0c98b0..f546c0013e0 100644 --- a/packages/web/src/pages/server-track-page/mobile/ServerTrackPage.tsx +++ b/packages/web/src/pages/server-track-page/mobile/ServerTrackPage.tsx @@ -15,7 +15,7 @@ import NavContext, { LeftPreset, CenterPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import { getTrackDefaults } from 'pages/track-page/utils' import { decodeHashId } from 'utils/hashIds' diff --git a/packages/web/src/pages/settings-page/components/mobile/ChangeEmailPage.tsx b/packages/web/src/pages/settings-page/components/mobile/ChangeEmailPage.tsx index c92c6f0f534..08633649da1 100644 --- a/packages/web/src/pages/settings-page/components/mobile/ChangeEmailPage.tsx +++ b/packages/web/src/pages/settings-page/components/mobile/ChangeEmailPage.tsx @@ -13,7 +13,7 @@ import { NewEmailPage, VerifyEmailPage } from 'components/change-email/ChangeEmailModal' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import { ToastContext } from 'components/toast/ToastContext' import { SettingsPageProps } from './SettingsPage' diff --git a/packages/web/src/pages/settings-page/components/mobile/ChangePasswordPage.tsx b/packages/web/src/pages/settings-page/components/mobile/ChangePasswordPage.tsx index be7ee5c4b4d..803ae6c8bd1 100644 --- a/packages/web/src/pages/settings-page/components/mobile/ChangePasswordPage.tsx +++ b/packages/web/src/pages/settings-page/components/mobile/ChangePasswordPage.tsx @@ -20,7 +20,7 @@ import { ConfirmCredentialsPage, NewPasswordPage } from 'components/change-password/ChangePasswordModal' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import { ToastContext } from 'components/toast/ToastContext' import { SettingsPageProps } from './SettingsPage' diff --git a/packages/web/src/pages/settings-page/components/mobile/SettingsPage.tsx b/packages/web/src/pages/settings-page/components/mobile/SettingsPage.tsx index 845124017ab..a00b17a237f 100644 --- a/packages/web/src/pages/settings-page/components/mobile/SettingsPage.tsx +++ b/packages/web/src/pages/settings-page/components/mobile/SettingsPage.tsx @@ -26,7 +26,7 @@ import DynamicImage from 'components/dynamic-image/DynamicImage' import GroupableList from 'components/groupable-list/GroupableList' import Grouping from 'components/groupable-list/Grouping' import Row from 'components/groupable-list/Row' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import Page from 'components/page/Page' import useScrollToTop from 'hooks/useScrollToTop' import { useUserProfilePicture } from 'hooks/useUserProfilePicture' diff --git a/packages/web/src/pages/supporting-page/SupportingPage.tsx b/packages/web/src/pages/supporting-page/SupportingPage.tsx index 7a20038daaa..8ed67e7728d 100644 --- a/packages/web/src/pages/supporting-page/SupportingPage.tsx +++ b/packages/web/src/pages/supporting-page/SupportingPage.tsx @@ -6,7 +6,7 @@ import { } from '@audius/common/store' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' const { getUserList } = supportingUserListSelectors diff --git a/packages/web/src/pages/top-supporters-page/TopSupportersPage.tsx b/packages/web/src/pages/top-supporters-page/TopSupportersPage.tsx index adbb234b61b..2c7a9507ca1 100644 --- a/packages/web/src/pages/top-supporters-page/TopSupportersPage.tsx +++ b/packages/web/src/pages/top-supporters-page/TopSupportersPage.tsx @@ -6,7 +6,7 @@ import { } from '@audius/common/store' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import NavContext, { LeftPreset } from 'components/nav/store/context' +import NavContext, { LeftPreset } from 'components/nav/mobile/NavContext' import UserList from 'components/user-list/UserList' const { getUserList } = topSupportersUserListSelectors diff --git a/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx b/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx index d3871c1e076..59c343b84a5 100644 --- a/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx +++ b/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx @@ -16,7 +16,7 @@ import NavContext, { LeftPreset, CenterPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import SectionButton from 'components/section-button/SectionButton' import { getTrackDefaults } from 'pages/track-page/utils' diff --git a/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx b/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx index 93145654350..1bcf1f43944 100644 --- a/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx +++ b/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx @@ -20,7 +20,7 @@ import NavContext, { CenterPreset, LeftPreset, RightPreset -} from 'components/nav/store/context' +} from 'components/nav/mobile/NavContext' import useTabs from 'hooks/useTabs/useTabs' import { TrendingPageContentProps } from 'pages/trending-page/types' import { BASE_URL, TRENDING_PAGE } from 'utils/route'