mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 17:59:13 -04:00
Add "Get Notified" button for saved searches and improve filters UI behavior:
- Introduced `GetNotifiedButton` for bookmarking searches and receiving notifications when new profiles match. - Optimized responsive behavior of filters for desktop and mobile layouts. - Enhanced "no profiles found" state with a clear call-to-action and improved visual design. - Updated translations for notification strings in German and French.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<Col className={'text-ink-600 w-full gap-2 py-2 text-sm main-font'}>
|
||||
<Row className={'mb-2 justify-between gap-2'}>
|
||||
<Row className="mb-2 items-center justify-center gap-1.5 flex-wrap">
|
||||
<Input
|
||||
ref={ref}
|
||||
value={keywordInput}
|
||||
placeholder={placeholder}
|
||||
className={'w-full sm:w-96'}
|
||||
className="w-full max-w-md !h-14 !rounded-full !px-5 text-base shadow-md"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setKeywordInput(e.target.value)
|
||||
}}
|
||||
searchIcon
|
||||
/>
|
||||
|
||||
<Row className="gap-2">
|
||||
<Row className="gap-1.5 shrink-0 items-center">
|
||||
<Select
|
||||
ref={sortSelectRef}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
@@ -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',
|
||||
)}
|
||||
>
|
||||
<option value="created_time">{t('common.new', 'New')}</option>
|
||||
{youProfile && (
|
||||
@@ -195,57 +190,50 @@ export const Search = forwardRef<
|
||||
<option value="last_online_time">{t('common.active', 'Active')}</option>
|
||||
</Select>
|
||||
<Button
|
||||
color={highlightFilters ? 'blue' : 'none'}
|
||||
color={highlightFilters ? 'blue' : 'gray-white'}
|
||||
size="sm"
|
||||
className={`border-ink-300 border lg:hidden${highlightFilters ? ' border-blue-500' : ''}`}
|
||||
className={clsx(
|
||||
'!h-10 !rounded-full border border-canvas-200',
|
||||
highlightFilters && 'border-blue-500',
|
||||
)}
|
||||
onClick={handleOpenFilters}
|
||||
>
|
||||
<IoFilterSharp className="h-5 w-5" />
|
||||
<IoFilterSharp className="h-4 w-4 sm:mr-1.5" />
|
||||
<span className="hidden sm:inline">{t('search.filters', 'Filters')}</span>
|
||||
</Button>
|
||||
<GetNotifiedButton
|
||||
filters={filters}
|
||||
locationFilterProps={locationFilterProps}
|
||||
bookmarkedSearches={bookmarkedSearches}
|
||||
refreshBookmarkedSearches={refreshBookmarkedSearches}
|
||||
onSaved={() => setOpenBookmarks(true)}
|
||||
iconOnly
|
||||
size="sm"
|
||||
color="gray-white"
|
||||
className="!h-10 !w-10 !rounded-full border border-canvas-200 !p-0"
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<RightModal
|
||||
className="bg-canvas-50 w-2/3 text-sm lg:hidden h-full max-h-screen overflow-y-auto"
|
||||
open={openFiltersModal}
|
||||
className="bg-canvas-50 w-full sm:w-96 text-sm h-full max-h-screen overflow-y-auto"
|
||||
open={openFiltersModal && !suppressFiltersModal}
|
||||
setOpen={setOpenFiltersModal}
|
||||
>
|
||||
<Row className="items-center justify-between px-3 pt-3">
|
||||
<span className="font-medium text-ink-900">{t('search.filters', 'Filters')}</span>
|
||||
<Button
|
||||
size="2xs"
|
||||
color="gray-white"
|
||||
onClick={() => setOpenFiltersModal(false)}
|
||||
aria-label={t('common.close', 'Close')}
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Row>
|
||||
{filtersElement}
|
||||
</RightModal>
|
||||
<Row className="items-center justify-between w-full flex-wrap gap-2">
|
||||
<Row className={'mb-2 gap-2'}>
|
||||
<Button
|
||||
disabled={loadingBookmark}
|
||||
loading={loadingBookmark}
|
||||
onClick={() => {
|
||||
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'}
|
||||
>
|
||||
<Bell className="h-4 w-4 mr-1 hidden sm:flex" />{' '}
|
||||
{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')}
|
||||
</Button>
|
||||
|
||||
<BookmarkSearchButton
|
||||
refreshBookmarkedSearches={refreshBookmarkedSearches}
|
||||
bookmarkedSearches={bookmarkedSearches}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {JSONContent} from '@tiptap/core'
|
||||
import clsx from 'clsx'
|
||||
import {INVERTED_DIET_CHOICES, INVERTED_LANGUAGE_CHOICES} from 'common/choices'
|
||||
import {FilterFields} from 'common/filters'
|
||||
import {Gender} from 'common/gender'
|
||||
import {CompatibilityScore} from 'common/profiles/compatibility-score'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
HandHeart,
|
||||
Languages,
|
||||
Salad,
|
||||
SearchX,
|
||||
Sparkles,
|
||||
Wine,
|
||||
} from 'lucide-react'
|
||||
@@ -20,17 +22,20 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import {PiMagnifyingGlassBold} from 'react-icons/pi'
|
||||
import {LocationFilterProps} from 'web/components/filters/location-filter'
|
||||
import GenderIcon from 'web/components/gender-icon'
|
||||
import {IconWithInfo} from 'web/components/icons'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {SendMessageButton} from 'web/components/messaging/send-message-button'
|
||||
import {ProfileLocation} from 'web/components/profile/profile-location'
|
||||
import {getSeekingText} from 'web/components/profile-about'
|
||||
import {GetNotifiedButton} from 'web/components/searches/get-notified-button'
|
||||
import {CompatibleBadge} from 'web/components/widgets/compatible-badge'
|
||||
import {Content} from 'web/components/widgets/editor'
|
||||
import HideProfileButton from 'web/components/widgets/hide-profile-button'
|
||||
import {StarButton} from 'web/components/widgets/star-button'
|
||||
import {LoadMoreUntilNotVisible} from 'web/components/widgets/visibility-observer'
|
||||
import {BookmarkedSearchesType} from 'web/hooks/use-bookmarked-searches'
|
||||
import {useChoicesContext} from 'web/hooks/use-choices'
|
||||
import {isDark, useTheme} from 'web/hooks/use-theme'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
@@ -91,6 +96,10 @@ export const ProfileGrid = (props: {
|
||||
hiddenUserIds?: string[]
|
||||
onUndoHidden?: (userId: string) => void
|
||||
displayOptions?: Partial<DisplayOptions>
|
||||
filters: Partial<FilterFields>
|
||||
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 && (
|
||||
<div className="py-8 text-center">
|
||||
<p>{t('profile_grid.no_profiles', 'No profiles found.')}</p>
|
||||
<p>
|
||||
<Col className="items-center gap-3 py-16 px-4 text-center">
|
||||
<div className="rounded-full bg-canvas-200 p-4">
|
||||
<SearchX className="h-7 w-7 text-ink-400" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-ink-900">
|
||||
{t('profile_grid.no_profiles', 'No profiles found')}
|
||||
</p>
|
||||
<p className="max-w-sm text-ink-500">
|
||||
{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.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<GetNotifiedButton
|
||||
filters={filters}
|
||||
locationFilterProps={locationFilterProps}
|
||||
bookmarkedSearches={bookmarkedSearches}
|
||||
refreshBookmarkedSearches={refreshBookmarkedSearches}
|
||||
color="cta"
|
||||
size="lg"
|
||||
className="mt-1"
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -401,47 +428,54 @@ function ProfilePreview(props: {
|
||||
className={clsx('pt-1 text-xs', cardSize !== 'large' && 'hidden sm:flex')}
|
||||
/>
|
||||
)}
|
||||
{onHide && (
|
||||
<HideProfileButton
|
||||
hiddenUserId={profile.user_id}
|
||||
onHidden={onHide}
|
||||
className="ml-1"
|
||||
stopPropagation
|
||||
eyeOff
|
||||
onPointerDown={() => {
|
||||
hideButtonClickedRef.current = true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{hasStar !== undefined && (
|
||||
<StarButton
|
||||
targetProfile={profile}
|
||||
isStarred={hasStar}
|
||||
refresh={refreshStars}
|
||||
size={'h-4 w-4'}
|
||||
className="h-7 w-7 !rounded-lg !p-1 hover:border-primary-400 hover:bg-primary-50"
|
||||
onPointerDown={() => {
|
||||
hideButtonClickedRef.current = true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{user && (
|
||||
<div
|
||||
className={clsx(cardSize !== 'large' && 'hidden sm:flex')}
|
||||
data-testid="message-profile-button"
|
||||
>
|
||||
<SendMessageButton
|
||||
toUser={user}
|
||||
profile={profile}
|
||||
size={'h-4 w-4'}
|
||||
className={clsx('!p-1 w-7 h-7')}
|
||||
accentIfMessaged
|
||||
<Row
|
||||
className={clsx(
|
||||
'items-start gap-1 transition-opacity',
|
||||
'lg:opacity-0 lg:group-hover:opacity-100 lg:group-focus-within:opacity-100',
|
||||
)}
|
||||
>
|
||||
{onHide && (
|
||||
<HideProfileButton
|
||||
hiddenUserId={profile.user_id}
|
||||
onHidden={onHide}
|
||||
className="ml-1"
|
||||
stopPropagation
|
||||
eyeOff
|
||||
onPointerDown={() => {
|
||||
hideButtonClickedRef.current = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{hasStar !== undefined && (
|
||||
<StarButton
|
||||
targetProfile={profile}
|
||||
isStarred={hasStar}
|
||||
refresh={refreshStars}
|
||||
size={'h-4 w-4'}
|
||||
className="h-7 w-7 !rounded-lg !p-1 hover:border-primary-400 hover:bg-primary-50"
|
||||
onPointerDown={() => {
|
||||
hideButtonClickedRef.current = true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{user && (
|
||||
<div
|
||||
className={clsx(cardSize !== 'large' && 'hidden sm:flex')}
|
||||
data-testid="message-profile-button"
|
||||
>
|
||||
<SendMessageButton
|
||||
toUser={user}
|
||||
profile={profile}
|
||||
size={'h-4 w-4'}
|
||||
className={clsx('!p-1 w-7 h-7')}
|
||||
accentIfMessaged
|
||||
onPointerDown={() => {
|
||||
hideButtonClickedRef.current = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
|
||||
<div className={clsx('flex lg:flex-row h-full lg:justify-between', cardClass)}>
|
||||
|
||||
@@ -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<string[]>([])
|
||||
const {refreshHiddenProfiles} = useHiddenProfiles()
|
||||
@@ -224,9 +236,11 @@ export function ProfilesHome() {
|
||||
/>
|
||||
)
|
||||
|
||||
const showDockedFilters = isDesktop && openFiltersModal
|
||||
|
||||
return (
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-4">
|
||||
<Col className={'lg:col-span-9'}>
|
||||
<div className={clsx(showDockedFilters && 'lg:grid lg:grid-cols-12 lg:gap-4')}>
|
||||
<Col className={clsx(showDockedFilters && 'lg:col-span-9')}>
|
||||
{showSignupBanner && user && (
|
||||
<div className="w-full bg-canvas-50 rounded-xl text-center py-3 px-3 relative">
|
||||
<Col className="items-center justify-center gap-2">
|
||||
@@ -321,9 +335,10 @@ export function ProfilesHome() {
|
||||
<Title className="!mb-2 text-3xl">{t('profiles.title', 'People')}</Title>
|
||||
<Search
|
||||
ref={searchInputRef}
|
||||
openFilters={() => 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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
{/* Desktop: filters sidebar on the right */}
|
||||
<div className="hidden lg:block lg:col-span-3 lg:sticky lg:top-4 lg:h-fit text-sm bg-canvas-50 rounded-xl max-h-[calc(100vh-2rem)] overflow-y-auto">
|
||||
{filtersElement}
|
||||
</div>
|
||||
{showDockedFilters && (
|
||||
<div className="hidden lg:flex lg:flex-col lg:col-span-3 lg:sticky lg:top-4 lg:h-fit text-sm bg-canvas-50 rounded-xl max-h-[calc(100vh-2rem)] overflow-y-auto">
|
||||
<Row className="items-center justify-between px-3 pt-3">
|
||||
<span className="font-medium text-ink-900">{t('search.filters', 'Filters')}</span>
|
||||
<Button
|
||||
size="2xs"
|
||||
color="gray-white"
|
||||
onClick={() => setOpenFiltersModal(false)}
|
||||
aria-label={t('common.close', 'Close')}
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Row>
|
||||
{filtersElement}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
94
web/components/searches/get-notified-button.tsx
Normal file
94
web/components/searches/get-notified-button.tsx
Normal file
@@ -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<FilterFields>
|
||||
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 = (
|
||||
<Button
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
onClick={handleClick}
|
||||
size={size}
|
||||
color={color}
|
||||
className={className}
|
||||
aria-label={label}
|
||||
>
|
||||
<Bell className={clsx('h-4 w-4', !iconOnly && 'mr-1.5', bookmarked && 'fill-current')} />
|
||||
{!iconOnly && (loading ? '' : label)}
|
||||
</Button>
|
||||
)
|
||||
|
||||
return iconOnly ? <Tooltip text={label}>{button}</Tooltip> : button
|
||||
}
|
||||
Reference in New Issue
Block a user