Add bookmarked filters for notifications

This commit is contained in:
MartinBraquet
2025-09-13 23:21:45 +02:00
parent cb79e27d5a
commit 155d1f4c06
7 changed files with 270 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
import {Lover, LoverRow} from 'common/love/lover'
import {useEffect, useState} from 'react'
import React, {useEffect, useState} from 'react'
import {IoFilterSharp} from 'react-icons/io5'
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
@@ -10,6 +10,12 @@ import {Select} from 'web/components/widgets/select'
import {DesktopFilters} from './desktop-filters'
import {LocationFilterProps} from './location-filter'
import {MobileFilters} from './mobile-filters'
import {BookmarkSearchButton} from "web/components/searches/button";
import {BookmarkedSearchesType} from "web/hooks/use-bookmarked-searches";
import {submitBookmarkedSearch} from "web/lib/supabase/searches";
import {useUser} from "web/hooks/use-user";
import {isEqual} from "lodash";
import {initialFilters} from "web/components/filters/use-filters";
export type FilterFields = {
orderBy: 'last_online_time' | 'created_time' | 'compatibility_score'
@@ -57,6 +63,8 @@ export const Search = (props: {
setYourFilters: (checked: boolean) => void
isYourFilters: boolean
locationFilterProps: LocationFilterProps
bookmarkedSearches: BookmarkedSearchesType[]
refreshBookmarkedSearches: () => void
}) => {
const {
youLover,
@@ -66,6 +74,8 @@ export const Search = (props: {
isYourFilters,
locationFilterProps,
filters,
bookmarkedSearches,
refreshBookmarkedSearches,
} = props
const [openFiltersModal, setOpenFiltersModal] = useState(false)
@@ -74,6 +84,10 @@ export const Search = (props: {
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 user = useUser()
useEffect(() => {
if (isHolding) return;
@@ -100,8 +114,12 @@ export const Search = (props: {
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'}>
<Col className={'text-ink-600 w-full gap-2 py-2 text-sm main-font'}>
<Row className={'mb-2 justify-between gap-2'}>
<Input
value={filters.name ?? ''}
@@ -170,6 +188,35 @@ export const Search = (props: {
locationFilterProps={locationFilterProps}
/>
</RightModal>
<Row className={'mb-2 gap-2'}>
<Button
disabled={
loadingBookmark || isEqual(filters, initialFilters)
}
loading={loadingBookmark}
onClick={() => {
setLoadingBookmark(true)
submitBookmarkedSearch(filters, user?.id)
.finally(() => {
setLoadingBookmark(false)
setBookmarked(true)
refreshBookmarkedSearches()
setOpenBookmarks(true)
})
}}
size={'xs'}
color={'none'}
className={'bg-canvas-100 hover:bg-canvas-200'}
>
{bookmarked ? 'Bookmarked!' : loadingBookmark ? '' : 'Get Notified'}
</Button>
<BookmarkSearchButton
refreshBookmarkedSearches={refreshBookmarkedSearches}
bookmarkedSearches={bookmarkedSearches}
openBookmarks={openBookmarks}
/>
</Row>
</Col>
)
}

View File

@@ -39,7 +39,7 @@ export const orderLovers = (
// return filterDefined(zip(women, nonWomen).flat())
// }
const initialFilters: Partial<FilterFields> = {
export const initialFilters: Partial<FilterFields> = {
geodbCityIds: undefined,
name: undefined,
genders: undefined,

View File

@@ -34,6 +34,8 @@ export const ProfileGrid = (props: {
const user = useUser()
const other_lovers = lovers.filter((lover) => lover.user_id !== user?.id);
return (
<div className="relative">
<div
@@ -42,17 +44,16 @@ export const ProfileGrid = (props: {
isReloading && 'animate-pulse opacity-80'
)}
>
{lovers
.filter((lover) => lover.user_id !== user?.id)
{other_lovers
.map((lover) => (
<ProfilePreview
key={lover.id}
lover={lover}
compatibilityScore={compatibilityScores?.[lover.user_id]}
hasStar={starredUserIds?.includes(lover.user_id) ?? false}
refreshStars={refreshStars}
/>
))}
<ProfilePreview
key={lover.id}
lover={lover}
compatibilityScore={compatibilityScores?.[lover.user_id]}
hasStar={starredUserIds?.includes(lover.user_id) ?? false}
refreshStars={refreshStars}
/>
))}
</div>
<LoadMoreUntilNotVisible loadMore={loadMore}/>
@@ -63,8 +64,11 @@ export const ProfileGrid = (props: {
</div>
)}
{!isLoadingMore && !isReloading && lovers.length === 0 && (
<div className="py-8 text-center">No profiles found</div>
{!isLoadingMore && !isReloading && other_lovers.length === 0 && (
<div className="py-8 text-center">
<p>No profiles found.</p>
<p>Feel free to bookmark your search and we'll notify you when new users match it!</p>
</div>
)}
</div>
)
@@ -76,8 +80,8 @@ function ProfilePreview(props: {
hasStar: boolean
refreshStars: () => Promise<void>
}) {
const {lover, compatibilityScore, hasStar, refreshStars} = props
const {user, gender, age, pinned_url, city, bio} = lover
const {lover, compatibilityScore} = props
const {user} = lover
// const currentUser = useUser()
return (

View File

@@ -17,6 +17,7 @@ import {useUser} from 'web/hooks/use-user'
import {api} from 'web/lib/api'
import {debounce, omit} from 'lodash'
import {PREF_AGE_MAX, PREF_AGE_MIN,} from 'web/components/filters/location-filter'
import {useBookmarkedSearches} from "web/hooks/use-bookmarked-searches";
export function ProfilesHome() {
const user = useUser();
@@ -33,6 +34,7 @@ export function ProfilesHome() {
} = useFilters(you ?? undefined);
const [lovers, setLovers] = usePersistentInMemoryState<Lover[] | undefined>(undefined, 'profile-lovers');
const {bookmarkedSearches, refreshBookmarkedSearches} = useBookmarkedSearches(user?.id)
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [isReloading, setIsReloading] = useState(false);
@@ -105,6 +107,8 @@ export function ProfilesHome() {
setYourFilters={setYourFilters}
isYourFilters={isYourFilters}
locationFilterProps={locationFilterProps}
bookmarkedSearches={bookmarkedSearches}
refreshBookmarkedSearches={refreshBookmarkedSearches}
/>
{displayLovers === undefined || compatibleLovers === undefined ? (
<LoadingIndicator/>

View File

@@ -0,0 +1,122 @@
import {User} from "common/user";
import {useEffect, useState} from "react";
import {Button} from "web/components/buttons/button";
import {Modal, MODAL_CLASS} from "web/components/layout/modal";
import {Col} from "web/components/layout/col";
import {BookmarkedSearchesType} from "web/hooks/use-bookmarked-searches";
import {useUser} from "web/hooks/use-user";
import {initialFilters} from "web/components/filters/use-filters";
import {Row} from "web/components/layout/row";
import {deleteBookmarkedSearch} from "web/lib/supabase/searches";
import {FilterFields} from "web/components/filters/search";
export function BookmarkSearchButton(props: {
bookmarkedSearches: BookmarkedSearchesType[]
refreshBookmarkedSearches: () => void
openBookmarks?: boolean
}) {
const {
bookmarkedSearches,
refreshBookmarkedSearches,
openBookmarks,
} = props
const [open, setOpen] = useState(false)
const user = useUser()
useEffect(() => {
if (openBookmarks) setOpen(true)
}, [openBookmarks]);
if (!user) return null
return (
<>
<Button onClick={() => setOpen(true)} color="gray-outline" size={'xs'}>
My Bookmarked Searches
</Button>
<ButtonModal
open={open}
setOpen={setOpen}
user={user}
bookmarkedSearches={bookmarkedSearches}
refreshBookmarkedSearches={refreshBookmarkedSearches}
/>
</>
)
}
function ButtonModal(props: {
open: boolean
setOpen: (open: boolean) => void
user: User
bookmarkedSearches: BookmarkedSearchesType[]
refreshBookmarkedSearches: () => void
}) {
const {open, setOpen, bookmarkedSearches, refreshBookmarkedSearches} = props
return (
<Modal
open={open}
setOpen={setOpen}
onClose={() => {
refreshBookmarkedSearches()
}}
>
<Col className={MODAL_CLASS}>
<div>Bookmarked Searches</div>
<p className='text-xs'>We'll notify you daily when new people match your searches below.</p>
<Col
className={
'border-ink-300 text-ink-400 bg-canvas-0 inline-flex flex-col gap-2 rounded-md border p-1 text-sm shadow-sm'
}
>
{(bookmarkedSearches || []).map((search) => (
<Row key={search.id}>
<p>
{JSON.stringify(
Object.fromEntries(
Object.entries(search.search_filters as Record<string, any>).filter(([key, value]) => {
// skip null/undefined
if (value == null) return false
// skip empty arrays
if (Array.isArray(value) && value.length === 0) return false
// keep if different from initialFilters
return initialFilters[key as keyof FilterFields] !== value
})
)
)}
</p>
<button
onClick={async () => {
await deleteBookmarkedSearch(search.id)
refreshBookmarkedSearches()
}}
className="inline-flex h-4 w-4 items-center justify-center rounded-full text-red-600 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400"
>
×
</button>
</Row>
))}
</Col>
{/*<BookmarkSearchContent*/}
{/* total={bookmarkedSearches.length}*/}
{/* compatibilityQuestion={bookmarkedSearches[questionIndex]}*/}
{/* user={user}*/}
{/* onSubmit={() => {*/}
{/* setOpen(false)*/}
{/* }}*/}
{/* isLastQuestion={questionIndex === bookmarkedSearches.length - 1}*/}
{/* onNext={() => {*/}
{/* if (questionIndex === bookmarkedSearches.length - 1) {*/}
{/* setOpen(false)*/}
{/* } else {*/}
{/* setQuestionIndex(questionIndex + 1)*/}
{/* }*/}
{/* }}*/}
{/*/>*/}
</Col>
</Modal>
)
}

View File

@@ -0,0 +1,25 @@
import {usePersistentInMemoryState} from "web/hooks/use-persistent-in-memory-state";
import {Row} from "common/supabase/utils";
import {useEffect} from "react";
import {getUserBookmarkedSearches} from "web/lib/supabase/searches";
export const useBookmarkedSearches = (userId: string | undefined) => {
const [bookmarkedSearches, setBookmarkedSearches] = usePersistentInMemoryState<Row<'bookmarked_searches'>[]>(
[],
`bookmarked-searches-${userId}`
)
useEffect(() => {
if (userId) getUserBookmarkedSearches(userId).then(setBookmarkedSearches)
}, [userId])
async function refreshBookmarkedSearches() {
if (userId) getUserBookmarkedSearches(userId).then(setBookmarkedSearches)
}
return {bookmarkedSearches, refreshBookmarkedSearches}
}
export type BookmarkedSearchesType = Row<'bookmarked_searches'>

View File

@@ -0,0 +1,51 @@
import {Row, run} from "common/supabase/utils";
import {db} from "web/lib/supabase/db";
import {filterKeys} from "web/components/questions-form";
import {track} from "web/lib/service/analytics";
import {FilterFields} from "web/components/filters/search";
export const getUserBookmarkedSearches = async (userId: string) => {
const {data} = await run(
db
.from('bookmarked_searches')
.select('*')
.eq('creator_id', userId)
.order('id', {ascending: false})
)
return data
}
export type BookmarkedSearchSubmitType = Omit<
Row<'bookmarked_searches'>,
'created_time' | 'id' | 'last_notified_at'
>
export const submitBookmarkedSearch = async (
filters: Partial<FilterFields>,
userId: string | undefined | null
) => {
if (!filters) return
if (!userId) return
const row = {search_filters: filters, creator_id: userId}
const input = {
...filterKeys(row, (key, _) => !['id', 'created_time', 'last_notified_at'].includes(key)),
} as BookmarkedSearchSubmitType
await run(
db.from('bookmarked_searches').upsert(input)
).then(() => {
track('bookmarked_searches submit', {...filters})
})
}
export const deleteBookmarkedSearch = async (
id: number,
) => {
if (!id) return
await run(
db.from('bookmarked_searches').delete().eq('id', id)
).then(() => {
track('bookmarked_searches delete', {id})
})
}