diff --git a/web/components/filters/search.tsx b/web/components/filters/search.tsx index 415d420..5b530df 100644 --- a/web/components/filters/search.tsx +++ b/web/components/filters/search.tsx @@ -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 ( - + + + + + + ) } diff --git a/web/components/filters/use-filters.ts b/web/components/filters/use-filters.ts index 285da67..acfec24 100644 --- a/web/components/filters/use-filters.ts +++ b/web/components/filters/use-filters.ts @@ -39,7 +39,7 @@ export const orderLovers = ( // return filterDefined(zip(women, nonWomen).flat()) // } -const initialFilters: Partial = { +export const initialFilters: Partial = { geodbCityIds: undefined, name: undefined, genders: undefined, diff --git a/web/components/profile-grid.tsx b/web/components/profile-grid.tsx index 0f78833..55d27c4 100644 --- a/web/components/profile-grid.tsx +++ b/web/components/profile-grid.tsx @@ -34,6 +34,8 @@ export const ProfileGrid = (props: { const user = useUser() + const other_lovers = lovers.filter((lover) => lover.user_id !== user?.id); + return (
- {lovers - .filter((lover) => lover.user_id !== user?.id) + {other_lovers .map((lover) => ( - - ))} + + ))}
@@ -63,8 +64,11 @@ export const ProfileGrid = (props: {
)} - {!isLoadingMore && !isReloading && lovers.length === 0 && ( -
No profiles found
+ {!isLoadingMore && !isReloading && other_lovers.length === 0 && ( +
+

No profiles found.

+

Feel free to bookmark your search and we'll notify you when new users match it!

+
)} ) @@ -76,8 +80,8 @@ function ProfilePreview(props: { hasStar: boolean refreshStars: () => Promise }) { - 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 ( diff --git a/web/components/profiles/profiles-home.tsx b/web/components/profiles/profiles-home.tsx index 712356e..6c3db82 100644 --- a/web/components/profiles/profiles-home.tsx +++ b/web/components/profiles/profiles-home.tsx @@ -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(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 ? ( diff --git a/web/components/searches/button.tsx b/web/components/searches/button.tsx new file mode 100644 index 0000000..25feb70 --- /dev/null +++ b/web/components/searches/button.tsx @@ -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 ( + <> + + + + ) +} + +function ButtonModal(props: { + open: boolean + setOpen: (open: boolean) => void + user: User + bookmarkedSearches: BookmarkedSearchesType[] + refreshBookmarkedSearches: () => void +}) { + const {open, setOpen, bookmarkedSearches, refreshBookmarkedSearches} = props + return ( + { + refreshBookmarkedSearches() + }} + > + +
Bookmarked Searches
+

We'll notify you daily when new people match your searches below.

+ + {(bookmarkedSearches || []).map((search) => ( + +

+ {JSON.stringify( + Object.fromEntries( + Object.entries(search.search_filters as Record).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 + }) + + ) + )} +

+ +
+ + ))} + + {/* {*/} + {/* setOpen(false)*/} + {/* }}*/} + {/* isLastQuestion={questionIndex === bookmarkedSearches.length - 1}*/} + {/* onNext={() => {*/} + {/* if (questionIndex === bookmarkedSearches.length - 1) {*/} + {/* setOpen(false)*/} + {/* } else {*/} + {/* setQuestionIndex(questionIndex + 1)*/} + {/* }*/} + {/* }}*/} + {/*/>*/} + +
+ ) +} \ No newline at end of file diff --git a/web/hooks/use-bookmarked-searches.ts b/web/hooks/use-bookmarked-searches.ts new file mode 100644 index 0000000..c6c0fc2 --- /dev/null +++ b/web/hooks/use-bookmarked-searches.ts @@ -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[]>( + [], + `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'> \ No newline at end of file diff --git a/web/lib/supabase/searches.ts b/web/lib/supabase/searches.ts new file mode 100644 index 0000000..4e5c856 --- /dev/null +++ b/web/lib/supabase/searches.ts @@ -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, + 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}) + }) +} \ No newline at end of file