diff --git a/web/components/filters/mobile-filters.tsx b/web/components/filters/mobile-filters.tsx index 7a0c09c..823890a 100644 --- a/web/components/filters/mobile-filters.tsx +++ b/web/components/filters/mobile-filters.tsx @@ -43,6 +43,7 @@ import {LuCigarette, LuGraduationCap} from "react-icons/lu"; import {RiScales3Line} from "react-icons/ri"; import {PiHandsPrayingBold} from "react-icons/pi"; import {ResetFiltersButton} from "web/components/searches/button"; +import {FilterGuide} from "web/components/guidance"; function MobileFilters(props: { filters: Partial @@ -81,13 +82,9 @@ function MobileFilters(props: { return ( - {/**/} + + + void @@ -114,7 +116,12 @@ export const Search = (props: { bookmarkedSearches: BookmarkedSearchesType[] refreshBookmarkedSearches: () => void profileCount: number | undefined -}) => { + openFilters?: () => void + openFiltersModal?: boolean + highlightFilters?: boolean + highlightSort?: boolean + setOpenFiltersModal?: (open: boolean) => void +}>((props, ref) => { const { youProfile, updateFilter, @@ -128,9 +135,43 @@ export const Search = (props: { starredUsers, refreshStars, profileCount, + openFilters, + openFiltersModal: parentOpenFiltersModal, + setOpenFiltersModal: parentSetOpenFiltersModal, + highlightFilters, + highlightSort, } = props - const [openFiltersModal, setOpenFiltersModal] = useState(false) + const [internalOpenFiltersModal, setInternalOpenFiltersModal] = useState(false) + + const openFiltersModal = parentOpenFiltersModal ?? internalOpenFiltersModal + const setOpenFiltersModal = parentSetOpenFiltersModal ?? setInternalOpenFiltersModal + + const sortSelectRef = useRef(null) + + const handleOpenFilters = () => { + if (openFilters) { + openFilters() + } else { + setOpenFiltersModal(true) + } + } + + useEffect(() => { + if (highlightSort && sortSelectRef.current) { + setTimeout(() => { + if (sortSelectRef.current) { + sortSelectRef.current.focus() + // Try multiple approaches to open the dropdown + sortSelectRef.current.click() + const event = new MouseEvent('mousedown', {bubbles: true}) + sortSelectRef.current.dispatchEvent(event) + const event2 = new MouseEvent('click', {bubbles: true}) + sortSelectRef.current.dispatchEvent(event2) + } + }, 1000) + } + }, [highlightSort]) const [placeholder, setPlaceholder] = useState(''); const [textToType, setTextToType] = useState(getRandomPair()); @@ -143,10 +184,7 @@ export const Search = (props: { const [openStarBookmarks, setOpenStarBookmarks] = useState(false); const user = useUser() const youSeekingRelationship = youProfile?.pref_relation_styles?.includes('relationship') - const isClearedFilters = isEqual( - {...filters, orderBy: undefined}, - {...initialFilters, orderBy: undefined} - ) + const isClearedFilters = useIsClearedFilters(filters) const {choices: interestChoices} = useChoices('interests') const {choices: causeChoices} = useChoices('causes') const {choices: workChoices} = useChoices('work') @@ -189,6 +227,7 @@ export const Search = (props: { + )} ) -} +}) diff --git a/web/components/guidance.tsx b/web/components/guidance.tsx new file mode 100644 index 0000000..4b2ce6e --- /dev/null +++ b/web/components/guidance.tsx @@ -0,0 +1,16 @@ +import {useRouter} from "next/router"; +import clsx from "clsx"; +import {useT} from "web/lib/locale"; + +export function FilterGuide(props: { + className?: string +}) { + const {className} = props + const router = useRouter() + const {query} = router + const fromSignup = query.fromSignup === 'true' + const t = useT() + return fromSignup &&

+ {t('profiles.filter_guide', 'Filter below by intent, age, location, and more')} +

; +} \ No newline at end of file diff --git a/web/components/profiles/profiles-home.tsx b/web/components/profiles/profiles-home.tsx index ab3d0d6..6458bd0 100644 --- a/web/components/profiles/profiles-home.tsx +++ b/web/components/profiles/profiles-home.tsx @@ -15,6 +15,13 @@ import {api} from 'web/lib/api' import {useBookmarkedSearches} from "web/hooks/use-bookmarked-searches" import {useFilters} from "web/components/filters/use-filters" import {useLocale, useT} from "web/lib/locale"; +import {useIsClearedFilters} from "web/hooks/use-is-cleared-filters"; +import {Button} from "web/components/buttons/button"; +import {Col} from "web/components/layout/col"; +import {Row} from "web/components/layout/row"; +import {useRouter} from "next/router"; +import {useIsMobile} from "web/hooks/use-is-mobile"; +import toast from "react-hot-toast"; export function ProfilesHome() { const user = useUser() @@ -34,8 +41,18 @@ export function ProfilesHome() { const {bookmarkedSearches, refreshBookmarkedSearches} = useBookmarkedSearches(user?.id) const [isLoadingMore, setIsLoadingMore] = useState(false) const [isReloading, setIsReloading] = useState(false) + const [showBanner, setShowBanner] = useState(true) + const [openFiltersModal, setOpenFiltersModal] = useState(false) + const [highlightFilters, setHighlightFilters] = useState(false) + const [highlightSort, setHighlightSort] = useState(false) + const searchInputRef = useRef(null) const t = useT() const {locale} = useLocale() + const isClearedFilters = useIsClearedFilters(filters) + const router = useRouter() + const {query} = router + const fromSignup = query.fromSignup === 'true' + const isMobile = useIsMobile() // const [debouncedAgeRange, setRawAgeRange] = useState({ // min: filters.pref_age_min ?? PREF_AGE_MIN, @@ -80,13 +97,29 @@ export function ProfilesHome() { // const displayProfiles = profiles && orderProfiles(profiles, starredUserIds) const displayProfiles = profiles + const limit = 20 + const loadMore = useCallback(async () => { if (!profiles || isLoadingMore) return false + if (fromSignup && isClearedFilters && profiles.length <= limit) { + toast(t('profiles.search_tip', 'Tip: Searching first helps you find better matches. Scrolling endlessly reduces relevance.'), { + icon: '⚠️', + style: { + // background: 'rgba(128,128,128,0.15)', // light gray bg, adjust for dark mode + // color: 'rgba(0,0,0,0.75)', // dark text for light mode + padding: '8px 8px', + borderRadius: '8px', + // fontFamily: 'Inter, sans-serif', + fontSize: '14px', + }, + duration: 10000, + }); + } try { setIsLoadingMore(true) const lastProfile = profiles[profiles.length - 1] const result = await api('get-profiles', removeNullOrUndefinedProps({ - limit: 20, + limit, compatibleWithUserId: user?.id, after: lastProfile?.id.toString(), locale, @@ -105,9 +138,74 @@ export function ProfilesHome() { return ( <> + {showBanner && fromSignup && ( +
+ + {t('profiles.search_intention', 'Compass works best when you search with intention. Try using keywords or filters instead of scrolling.')} + + + {isMobile && } + + + +

{t('profiles.interactive_profiles', 'Profiles are interactive — click any card to learn more and reach out.')}

+
+ + +
+ )} {/*{user && !profile && }*/} {t("profiles.title", "People")} setOpenFiltersModal(true)} + openFiltersModal={openFiltersModal} + setOpenFiltersModal={setOpenFiltersModal} + highlightFilters={highlightFilters} + highlightSort={highlightSort} youProfile={you} starredUsers={starredUsers ?? []} refreshStars={refreshStars} @@ -123,7 +221,12 @@ export function ProfilesHome() { /> {displayProfiles === undefined || compatibleProfiles === undefined ? ( - ) : ( + ) : (<> + {isClearedFilters && +

+ {t('profiles.seeing_all_profiles', 'You are seeing all profiles. Use search or filters to narrow it down.')} +

+ } - )} + )} ) } diff --git a/web/components/widgets/select.tsx b/web/components/widgets/select.tsx index be9a9b6..0311a5c 100644 --- a/web/components/widgets/select.tsx +++ b/web/components/widgets/select.tsx @@ -1,10 +1,12 @@ import clsx from 'clsx' +import {forwardRef} from 'react' -export const Select = (props: JSX.IntrinsicElements['select']) => { +export const Select = forwardRef((props, ref) => { const { className, children, ...rest } = props return ( ) -} +}) diff --git a/web/hooks/use-is-cleared-filters.ts b/web/hooks/use-is-cleared-filters.ts new file mode 100644 index 0000000..9259164 --- /dev/null +++ b/web/hooks/use-is-cleared-filters.ts @@ -0,0 +1,12 @@ +import {FilterFields, initialFilters} from "common/filters"; +import {isEqual} from "lodash"; +import {useMemo} from "react"; + +export function useIsClearedFilters(filters: Partial): boolean { + return useMemo(() => + isEqual( + {...filters, orderBy: undefined}, + {...initialFilters, orderBy: undefined} + ), [filters] + ) +} \ No newline at end of file diff --git a/web/messages/de.json b/web/messages/de.json index bbbb91c..53f379e 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -699,6 +699,15 @@ "profile_grid.no_profiles": "Keine Profile gefunden.", "profile_grid.notification_cta": "Klicken Sie gern auf „Benachrichtigen“, und wir informieren Sie, sobald neue Nutzer Ihrer Suche entsprechen.", "profiles.title": "Personen", + "profiles.search_intention": "Compass funktioniert am besten, wenn Sie mit Absicht suchen. Versuchen Sie, Schlüsselwörter oder Filter anstelle des Scrollens zu verwenden.", + "profiles.try_keyword_search": "Schlüsselwortsuche versuchen", + "profiles.show_filters": "Filter anzeigen", + "profiles.sort_differently": "Anders sortieren", + "profiles.interactive_profiles": "Profile sind interaktiv — klicken Sie auf jede Karte, um mehr zu erfahren und Kontakt aufzunehmen.", + "profiles.dismiss": "Verwerfen", + "profiles.seeing_all_profiles": "Sie sehen alle Profile. Verwenden Sie Suche oder Filter, um einzugrenzen.", + "profiles.filter_guide": "Filtern Sie unten nach Absicht, Alter, Standort und mehr", + "profiles.search_tip": "Tipp: Suchen Sie zuerst, um bessere Übereinstimmungen zu finden. Endloses Scrollen reduziert die Relevanz.", "register.agreement.and": " und ", "register.agreement.prefix": "Mit der Registrierung akzeptiere ich die ", "register.agreement.suffix": ".", diff --git a/web/messages/fr.json b/web/messages/fr.json index 2089c1b..30e0716 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -699,6 +699,15 @@ "profile_grid.no_profiles": "Aucun profil trouvé.", "profile_grid.notification_cta": "N'hésitez pas à cliquer sur \"Recevoir notifs\" et nous vous préviendrons quand de nouveaux utilisateurs correspondront à votre recherche !", "profiles.title": "Personnes", + "profiles.search_intention": "Compass fonctionne mieux lorsque vous cherchez avec intention. Essayez d'utiliser des mots-clés ou des filtres au lieu de faire défiler.", + "profiles.try_keyword_search": "Essayer une recherche par mot-clé", + "profiles.show_filters": "Voir les filtres", + "profiles.sort_differently": "Trier différemment", + "profiles.interactive_profiles": "Les profils sont interactifs — cliquez sur n'importe quelle carte pour en savoir plus et entrer en contact.", + "profiles.dismiss": "Fermer", + "profiles.seeing_all_profiles": "Vous voyez tous les profils. Utilisez la recherche ou les filtres pour affiner.", + "profiles.filter_guide": "Filtrez ci-dessous par intention, âge, lieu et plus encore", + "profiles.search_tip": "Astuce : Cherchez d'abord par filtres et mot-clés pour trouver de meilleures correspondances. Le défilement sans fin réduit la pertinence.", "register.agreement.and": " et ", "register.agreement.prefix": "En vous inscrivant, j'accepte les ", "register.agreement.suffix": ".", diff --git a/web/styles/globals.css b/web/styles/globals.css index 989c98d..95b5cdf 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -491,7 +491,7 @@ ol > li { ol > li::marker { font-weight: 600; font-size: 1em; - color: #374151; /* pick a visible color */ + color: #374151; /* pick a visible color */ } /* Onboarding animations */ @@ -529,3 +529,7 @@ ol > li::marker { animation: fade-out-slow 3s ease-out forwards; } +.guidance { + opacity: 0.55; + font-size: 14px; +}