diff --git a/common/messages/de.json b/common/messages/de.json
index 6da19aec..e692867a 100644
--- a/common/messages/de.json
+++ b/common/messages/de.json
@@ -174,7 +174,7 @@
"common.new": "Neu",
"common.next": "Weiter",
"common.no": "Nein",
- "common.notified": "Benachrichtigt werden",
+ "common.notified": "Benachrichtigen, wenn diese Suche jemanden findet",
"common.notified_any": "Über neue Profile benachrichtigt werden",
"common.or": "Oder",
"common.people": "Personen",
diff --git a/common/messages/fr.json b/common/messages/fr.json
index 9e9f7f94..8e94f71b 100644
--- a/common/messages/fr.json
+++ b/common/messages/fr.json
@@ -174,7 +174,7 @@
"common.new": "Nouveau",
"common.next": "Suivant",
"common.no": "Non",
- "common.notified": "Recevoir notifs pour les filtres sélectionnés",
+ "common.notified": "Être notifié quand cette recherche trouve quelqu'un",
"common.notified_any": "Recevoir notifs pour tout nouveau profil",
"common.or": "Ou",
"common.people": "personnes",
diff --git a/web/components/filters/search.tsx b/web/components/filters/search.tsx
index 73f432e7..f49ae788 100644
--- a/web/components/filters/search.tsx
+++ b/web/components/filters/search.tsx
@@ -1,25 +1,23 @@
import {QuestionMarkCircleIcon} from '@heroicons/react/24/outline'
+import {XMarkIcon} from '@heroicons/react/24/solid'
+import clsx from 'clsx'
import {DisplayUser} from 'common/api/user-types'
import {FilterFields} from 'common/filters'
import {Profile} from 'common/profiles/profile'
import {debounce as debounceFn} from 'lodash'
-import {Bell} from 'lucide-react'
import {forwardRef, ReactElement, useEffect, useRef, useState} from 'react'
-import toast from 'react-hot-toast'
import {IoFilterSharp} from 'react-icons/io5'
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
import {RightModal} from 'web/components/layout/right-modal'
import {Row} from 'web/components/layout/row'
import {BookmarkSearchButton, BookmarkStarButton} from 'web/components/searches/button'
+import {GetNotifiedButton} from 'web/components/searches/get-notified-button'
import {Input} from 'web/components/widgets/input'
import {Select} from 'web/components/widgets/select'
import {Tooltip} from 'web/components/widgets/tooltip'
import {BookmarkedSearchesType} from 'web/hooks/use-bookmarked-searches'
-import {useIsClearedFilters} from 'web/hooks/use-is-cleared-filters'
-import {useUser} from 'web/hooks/use-user'
import {useT} from 'web/lib/locale'
-import {submitBookmarkedSearch} from 'web/lib/supabase/searches'
import {LocationFilterProps} from './location-filter'
@@ -27,8 +25,6 @@ function isOrderBy(input: string): input is FilterFields['orderBy'] {
return ['last_online_time', 'created_time', 'compatibility_score'].includes(input)
}
-const MAX_BOOKMARKED_SEARCHES = 10
-
export const Search = forwardRef<
HTMLInputElement,
{
@@ -47,6 +43,9 @@ export const Search = forwardRef<
highlightFilters?: boolean
highlightSort?: boolean
setOpenFiltersModal?: (open: boolean) => void
+ // True when the parent renders filtersElement as a docked column instead (desktop),
+ // so this component's own slide-over shouldn't also open.
+ suppressFiltersModal?: boolean
filtersElement: ReactElement
}
>((props, ref) => {
@@ -63,6 +62,7 @@ export const Search = forwardRef<
openFilters,
openFiltersModal: parentOpenFiltersModal,
setOpenFiltersModal: parentSetOpenFiltersModal,
+ suppressFiltersModal,
highlightFilters,
highlightSort,
filtersElement,
@@ -106,12 +106,8 @@ export const Search = forwardRef<
// const [textToType, setTextToType] = useState(getRandomPair())
// const [_, setCharIndex] = useState(0)
// const [isHolding, setIsHolding] = useState(false)
- const [bookmarked, setBookmarked] = useState(false)
- const [loadingBookmark, setLoadingBookmark] = useState(false)
const [openBookmarks, setOpenBookmarks] = useState(false)
const [openStarBookmarks, setOpenStarBookmarks] = useState(false)
- const user = useUser()
- const isClearedFilters = useIsClearedFilters(filters)
// const choices = useChoicesContext()
const [keywordInput, setKeywordInput] = useState(filters.name ?? '')
@@ -157,25 +153,21 @@ export const Search = forwardRef<
// return () => clearInterval(interval)
// }, [textToType, isHolding])
- useEffect(() => {
- setTimeout(() => setBookmarked(false), 2000)
- }, [bookmarked])
-
return (
-
+
) => {
setKeywordInput(e.target.value)
}}
searchIcon
/>
-
+
) => {
@@ -186,7 +178,10 @@ export const Search = forwardRef<
}
}}
value={filters.orderBy || 'created_time'}
- className={`w-18 border-ink-300 rounded-md${highlightSort ? ' border-blue-500 ring-2 ring-blue-300' : ''}`}
+ className={clsx(
+ '!h-10 w-auto !rounded-full !border-canvas-200 !bg-transparent !shadow-none text-xs text-ink-500',
+ highlightSort && 'border-blue-500 ring-2 ring-blue-300',
+ )}
>
{t('common.new', 'New')}
{youProfile && (
@@ -195,57 +190,50 @@ export const Search = forwardRef<
{t('common.active', 'Active')}
-
+
+ {t('search.filters', 'Filters')}
+ setOpenBookmarks(true)}
+ iconOnly
+ size="sm"
+ color="gray-white"
+ className="!h-10 !w-10 !rounded-full border border-canvas-200 !p-0"
+ />
+
+ {t('search.filters', 'Filters')}
+ setOpenFiltersModal(false)}
+ aria-label={t('common.close', 'Close')}
+ >
+
+
+
{filtersElement}
- {
- if (bookmarkedSearches.length >= MAX_BOOKMARKED_SEARCHES) {
- toast.error(
- `You can bookmark maximum ${MAX_BOOKMARKED_SEARCHES} searches; please delete one first.`,
- )
- setOpenBookmarks(true)
- return
- }
- setLoadingBookmark(true)
- submitBookmarkedSearch(filters, locationFilterProps, user?.id).finally(() => {
- setLoadingBookmark(false)
- setBookmarked(true)
- refreshBookmarkedSearches()
- setOpenBookmarks(true)
- })
- }}
- // size={'xs'}
- color={'none'}
- className={'text-white bg-cta hover:bg-cta-hover rounded-xl text-xs transition-colors'}
- >
- {' '}
- {bookmarked
- ? t('common.saved', 'Saved!')
- : loadingBookmark
- ? ''
- : isClearedFilters
- ? t('common.notified_any', 'Get notified for any new profile')
- : t('common.notified', 'Get notified for selected filters')}
-
-
void
displayOptions?: Partial
+ filters: Partial
+ locationFilterProps: LocationFilterProps
+ bookmarkedSearches: BookmarkedSearchesType[]
+ refreshBookmarkedSearches: () => void
}) => {
const {
profiles,
@@ -104,6 +113,10 @@ export const ProfileGrid = (props: {
hiddenUserIds,
onUndoHidden,
displayOptions,
+ filters,
+ locationFilterProps,
+ bookmarkedSearches,
+ refreshBookmarkedSearches,
} = props
const {cardSize} = displayOptions ?? {}
@@ -155,15 +168,29 @@ export const ProfileGrid = (props: {
!isLoadingMore &&
!isReloading &&
other_profiles.length === 0 && (
-
-
{t('profile_grid.no_profiles', 'No profiles found.')}
-
+
+
+
+
+
+ {t('profile_grid.no_profiles', 'No profiles found')}
+
+
{t(
'profile_grid.notification_cta',
- "Feel free to click on Get Notified and we'll notify you when new users match your search!",
+ "We'll notify you as soon as someone new matches your search.",
)}
-
+
+
)
)}
@@ -401,47 +428,54 @@ function ProfilePreview(props: {
className={clsx('pt-1 text-xs', cardSize !== 'large' && 'hidden sm:flex')}
/>
)}
- {onHide && (
- {
- hideButtonClickedRef.current = true
- }}
- />
- )}
- {hasStar !== undefined && (
- {
- hideButtonClickedRef.current = true
- }}
- />
- )}
- {user && (
-
-
+ {onHide && (
+ {
hideButtonClickedRef.current = true
}}
/>
-
- )}
+ )}
+ {hasStar !== undefined && (
+ {
+ hideButtonClickedRef.current = true
+ }}
+ />
+ )}
+ {user && (
+
+ {
+ hideButtonClickedRef.current = true
+ }}
+ />
+
+ )}
+
diff --git a/web/components/profiles/profiles-home.tsx b/web/components/profiles/profiles-home.tsx
index 8569df17..f1e3d5e4 100644
--- a/web/components/profiles/profiles-home.tsx
+++ b/web/components/profiles/profiles-home.tsx
@@ -1,3 +1,5 @@
+import {XMarkIcon} from '@heroicons/react/24/solid'
+import clsx from 'clsx'
import {debug} from 'common/logger'
import {Profile} from 'common/profiles/profile'
import {removeNullOrUndefinedProps} from 'common/util/object'
@@ -77,6 +79,16 @@ export function ProfilesHome() {
const {locale} = useLocale()
const isClearedFilters = useIsClearedFilters(filters)
const isMobile = useIsMobile()
+ // Tracked separately from `isMobile` (640px) since the docked filters column only replaces
+ // the slide-over once there's room beside the grid, at the `lg` (1024px) breakpoint.
+ const [isDesktop, setIsDesktop] = useState(false)
+ useEffect(() => {
+ const mql = window.matchMedia('(min-width: 1024px)')
+ const update = () => setIsDesktop(mql.matches)
+ update()
+ mql.addEventListener('change', update)
+ return () => mql.removeEventListener('change', update)
+ }, [])
const [sendScrollWarning, setSendScrollWarning] = useState(true)
const [recentlyHiddenIds, setRecentlyHiddenIds] = useState
([])
const {refreshHiddenProfiles} = useHiddenProfiles()
@@ -224,9 +236,11 @@ export function ProfilesHome() {
/>
)
+ const showDockedFilters = isDesktop && openFiltersModal
+
return (
-
-
+
+
{showSignupBanner && user && (
@@ -321,9 +335,10 @@ export function ProfilesHome() {
{t('profiles.title', 'People')}
setOpenFiltersModal(true)}
+ openFilters={() => setOpenFiltersModal((open) => !open)}
openFiltersModal={openFiltersModal}
setOpenFiltersModal={setOpenFiltersModal}
+ suppressFiltersModal={isDesktop}
highlightFilters={highlightFilters}
highlightSort={highlightSort}
youProfile={you}
@@ -361,14 +376,30 @@ export function ProfilesHome() {
hiddenUserIds={recentlyHiddenIds}
onUndoHidden={onUndoHidden}
displayOptions={displayOptions}
+ filters={filters}
+ locationFilterProps={locationFilterProps}
+ bookmarkedSearches={bookmarkedSearches}
+ refreshBookmarkedSearches={refreshBookmarkedSearches}
/>
>
)}
- {/* Desktop: filters sidebar on the right */}
-
- {filtersElement}
-
+ {showDockedFilters && (
+
+
+ {t('search.filters', 'Filters')}
+ setOpenFiltersModal(false)}
+ aria-label={t('common.close', 'Close')}
+ >
+
+
+
+ {filtersElement}
+
+ )}
)
}
diff --git a/web/components/searches/get-notified-button.tsx b/web/components/searches/get-notified-button.tsx
new file mode 100644
index 00000000..a4ac9e8e
--- /dev/null
+++ b/web/components/searches/get-notified-button.tsx
@@ -0,0 +1,94 @@
+import clsx from 'clsx'
+import {FilterFields} from 'common/filters'
+import {Bell} from 'lucide-react'
+import {useEffect, useState} from 'react'
+import toast from 'react-hot-toast'
+import {Button, ColorType, SizeType} from 'web/components/buttons/button'
+import {LocationFilterProps} from 'web/components/filters/location-filter'
+import {Tooltip} from 'web/components/widgets/tooltip'
+import {BookmarkedSearchesType} from 'web/hooks/use-bookmarked-searches'
+import {useIsClearedFilters} from 'web/hooks/use-is-cleared-filters'
+import {useUser} from 'web/hooks/use-user'
+import {useT} from 'web/lib/locale'
+import {submitBookmarkedSearch} from 'web/lib/supabase/searches'
+
+const MAX_BOOKMARKED_SEARCHES = 10
+
+// Saves the current search as a bookmarked search so the user gets notified when new
+// profiles match it. Shared between the top search bar (icon) and the empty results state
+// (full CTA) so the save/loading/"Saved!" behavior stays identical in both places.
+export function GetNotifiedButton(props: {
+ filters: Partial
+ locationFilterProps: LocationFilterProps
+ bookmarkedSearches: BookmarkedSearchesType[]
+ refreshBookmarkedSearches: () => void
+ onSaved?: () => void
+ iconOnly?: boolean
+ size?: SizeType
+ color?: ColorType
+ className?: string
+}) {
+ const {
+ filters,
+ locationFilterProps,
+ bookmarkedSearches,
+ refreshBookmarkedSearches,
+ onSaved,
+ iconOnly,
+ size = 'md',
+ color = 'none',
+ className,
+ } = props
+
+ const user = useUser()
+ const t = useT()
+ const isClearedFilters = useIsClearedFilters(filters)
+ const [bookmarked, setBookmarked] = useState(false)
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ if (!bookmarked) return
+ const timeout = setTimeout(() => setBookmarked(false), 2000)
+ return () => clearTimeout(timeout)
+ }, [bookmarked])
+
+ const label = bookmarked
+ ? t('common.saved', 'Saved!')
+ : isClearedFilters
+ ? t('common.notified_any', 'Get notified for any new profile')
+ : t('common.notified', 'Notify me when this search matches someone')
+
+ const handleClick = () => {
+ if (bookmarkedSearches.length >= MAX_BOOKMARKED_SEARCHES) {
+ toast.error(
+ `You can bookmark maximum ${MAX_BOOKMARKED_SEARCHES} searches; please delete one first.`,
+ )
+ onSaved?.()
+ return
+ }
+ setLoading(true)
+ submitBookmarkedSearch(filters, locationFilterProps, user?.id).finally(() => {
+ setLoading(false)
+ setBookmarked(true)
+ refreshBookmarkedSearches()
+ onSaved?.()
+ })
+ }
+
+ const button = (
+
+
+ {!iconOnly && (loading ? '' : label)}
+
+ )
+
+ return iconOnly ? {button} : button
+}