mirror of
https://github.com/CompassConnections/Compass.git
synced 2025-12-23 22:18:43 -05:00
Add bookmarked filters for notifications
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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/>
|
||||
|
||||
122
web/components/searches/button.tsx
Normal file
122
web/components/searches/button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
web/hooks/use-bookmarked-searches.ts
Normal file
25
web/hooks/use-bookmarked-searches.ts
Normal 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'>
|
||||
51
web/lib/supabase/searches.ts
Normal file
51
web/lib/supabase/searches.ts
Normal 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})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user