Add profile filter: has photos

This commit is contained in:
MartinBraquet
2026-03-08 18:11:28 +01:00
parent d4de56873f
commit b445db6116
8 changed files with 90 additions and 10 deletions

View File

@@ -1,6 +1,5 @@
import * as Sentry from '@sentry/node'
import {type APIHandler} from 'api/helpers/endpoint'
import {debug} from 'common/logger'
import {OptionTableKey} from 'common/profiles/constants'
import {compact} from 'lodash'
import {convertRow} from 'shared/profiles/supabase'
@@ -34,6 +33,8 @@ export type profileQueryType = {
name?: string | undefined
/** Filter by gender identity */
genders?: string[] | undefined
/** Filter for profiles with photos */
hasPhoto?: boolean | undefined
/** Filter by education level */
education_levels?: string[] | undefined
/** Filter by preferred gender for matches */
@@ -89,13 +90,14 @@ export type profileQueryType = {
export const loadProfiles = async (props: profileQueryType) => {
const pg = createSupabaseDirectClient()
debug('loadProfiles', props)
console.debug('get-profiles', props)
const {
limit: limitParam,
after,
name,
userId,
genders,
hasPhoto,
education_levels,
pref_gender,
pref_age_min,
@@ -429,6 +431,8 @@ export const loadProfiles = async (props: profileQueryType) => {
`,
),
hasPhoto && where("pinned_url IS NOT NULL AND pinned_url != ''"),
lastModificationWithin &&
where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {
lastModificationWithin,

View File

@@ -359,10 +359,16 @@
"filter.last_active.3days": "Letzte 3 Tage",
"filter.last_active.week": "Letzte Woche",
"filter.last_active.month": "Letzter Monat",
"filter.last_active.now": "Jetzt online",
"filter.last_active.3months": "Letzte 3 Monate",
"filter.reset": "Zurücksetzen",
"filter.short_bio_toggle": "Kurze Bios einschließen",
"filter.show_photos": "Profilfotos anzeigen",
"filter.has_photo": "Hat Foto",
"filter.has_photo.title": "Hat Foto",
"filter.has_photo.on": "Ja",
"filter.has_photo.off": "Nein",
"filter.has_photo.photos": "Fotos",
"filter.wants_kids.any_preference": "Alle Präferenzen",
"filter.wants_kids.doesnt_want_kids": "Möchte keine Kinder",
"filter.wants_kids.either": "Egal",
@@ -966,6 +972,7 @@
"filter.group.values": "Werte & Überzeugungen",
"filter.group.personality": "Persönlichkeit",
"filter.group.advanced": "Erweitert",
"filter.group.rendering": "Darstellung",
"referrals.title": "Lade jemanden ein, Compass beizutreten!",
"register.agreement.and": " und ",
"register.agreement.prefix": "Mit der Registrierung akzeptiere ich die ",

View File

@@ -359,10 +359,16 @@
"filter.last_active.3days": "3 derniers jours",
"filter.last_active.week": "Dernière semaine",
"filter.last_active.month": "Dernier mois",
"filter.last_active.now": "En ligne",
"filter.last_active.3months": "3 derniers mois",
"filter.reset": "Réinitialiser",
"filter.short_bio_toggle": "Inclure les profils incomplets",
"filter.show_photos": "Afficher les photos",
"filter.has_photo": "A des photos",
"filter.has_photo.title": "A des photos",
"filter.has_photo.on": "Oui",
"filter.has_photo.off": "Non",
"filter.has_photo.photos": "Photos",
"filter.wants_kids.any_preference": "N'importe",
"filter.wants_kids.doesnt_want_kids": "Ne veut pas d'enfants",
"filter.wants_kids.either": "N'importe",
@@ -965,6 +971,7 @@
"filter.group.values": "Valeurs & Croyances",
"filter.group.personality": "Personnalité",
"filter.group.advanced": "Avancé",
"filter.group.rendering": "Rendu",
"referrals.title": "Invitez quelqu'un à rejoindre Compass !",
"register.agreement.and": " et ",
"register.agreement.prefix": "En vous inscrivant, j'accepte les ",

View File

@@ -761,6 +761,7 @@ export const API = (_apiTypeCheck = {
has_kids: z.coerce.number().optional(),
is_smoker: zBoolean.optional().optional(),
shortBio: zBoolean.optional().optional(),
hasPhoto: zBoolean.optional().optional(),
geodbCityIds: arraybeSchema.optional(),
lat: z.coerce.number().optional(),
lon: z.coerce.number().optional(),

View File

@@ -37,6 +37,7 @@ const filterLabels: Record<string, string> = {
work: '',
religion: '',
orderBy: '',
hasPhoto: '',
diet: 'Diet',
political_beliefs: 'Political views',
languages: '',
@@ -148,6 +149,8 @@ export function formatFilters(
`filter.last_active.${value}`,
LAST_ONLINE_CHOICES[value as keyof typeof LAST_ONLINE_CHOICES] ?? value,
)
else if (key === 'hasPhoto')
stringValue = value && translate('filter.has_photo.title', 'Has photos')
if (Array.isArray(value)) {
if (choicesIdsToLabels[key]) {

View File

@@ -18,6 +18,7 @@ export type FilterFields = {
mbti: string[] | null | undefined
name: string | null | undefined
shortBio: boolean | null | undefined
hasPhoto: boolean | null | undefined
drinks_min: number | null | undefined
drinks_max: number | null | undefined
// Big Five personality filters (0-100 range)

View File

@@ -17,6 +17,7 @@ import {
} from 'web/components/filters/big5-filter'
import {DietFilter, DietFilterText} from 'web/components/filters/diet-filter'
import {EducationFilter, EducationFilterText} from 'web/components/filters/education-filter'
import {HasPhotoFilter} from 'web/components/filters/has-photo-filter'
import {InterestFilter, InterestFilterText} from 'web/components/filters/interest-filter'
import {LanguageFilter, LanguageFilterText} from 'web/components/filters/language-filter'
import {MbtiFilter, MbtiFilterText} from 'web/components/filters/mbti-filter'
@@ -253,14 +254,6 @@ function Filters(props: {
<ShortBioToggle updateFilter={updateFilter} filters={filters} hidden={false} />
</Col>
{/* Show Photos */}
<Col className="p-4 pt-0">
<ShowPhotosToggle
updateRenderingOptions={updateRenderingOptions}
renderingOptions={renderingOptions}
/>
</Col>
{/* ALWAYS VISIBLE FILTERS */}
{/* CONNECTION - Always visible */}
@@ -731,6 +724,38 @@ function Filters(props: {
>
<LastActiveFilter filters={filters} updateFilter={updateFilter} />
</FilterSection>
{/* HAS PHOTO */}
<FilterSection
title={t('filter.has_photo.title', 'Has photos')}
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={!!filters.hasPhoto}
selection={
<span className={clsx(!filters.hasPhoto ? 'text-ink-900' : 'text-primary-600')}>
{filters.hasPhoto
? t('filter.has_photo', 'Has photos')
: t('filter.has_photo.photos', 'Photos')}
</span>
}
>
<HasPhotoFilter filters={filters} updateFilter={updateFilter} />
</FilterSection>
</FilterGroup>
{/* Rendering */}
<FilterGroup
title={t('filter.group.rendering', 'Rendering')}
openGroup={openGroup}
setOpenGroup={setOpenGroup}
>
{/* Show Photos */}
<Col className="p-4 pt-0">
<ShowPhotosToggle
updateRenderingOptions={updateRenderingOptions}
renderingOptions={renderingOptions}
/>
</Col>
</FilterGroup>
</Col>
)

View File

@@ -0,0 +1,32 @@
import clsx from 'clsx'
import {FilterFields} from 'common/filters'
import {Row} from 'web/components/layout/row'
import {useT} from 'web/lib/locale'
export function HasPhotoFilter(props: {
filters: Partial<FilterFields>
updateFilter: (newState: Partial<FilterFields>) => void
}) {
const {filters, updateFilter} = props
const t = useT()
const label = t('filter.has_photo', 'Has photos')
const on = filters.hasPhoto ?? false
return (
<Row className={clsx('mx-2 items-center hover-bold', on && 'font-semibold')}>
<input
id={label}
type="checkbox"
className="border-ink-300 bg-canvas-0 dark:border-ink-500 text-primary-600 focus:ring-primary-500 h-4 w-4 rounded hover:bg-canvas-200 cursor-pointer"
checked={on}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateFilter({hasPhoto: e.target.checked ? true : undefined})
}
/>
<label htmlFor={label} className={clsx('text-ink-600 ml-2')}>
{label}
</label>
</Row>
)
}