Add filter for education level

This commit is contained in:
MartinBraquet
2025-10-24 16:19:37 +02:00
parent acdd82a680
commit 591798e98c
13 changed files with 168 additions and 29 deletions

View File

@@ -12,6 +12,7 @@ export type profileQueryType = {
// Search and filter parameters
name?: string | undefined,
genders?: String[] | undefined,
education_levels?: String[] | undefined,
pref_gender?: String[] | undefined,
pref_age_min?: number | undefined,
pref_age_max?: number | undefined,
@@ -44,6 +45,7 @@ export const loadProfiles = async (props: profileQueryType) => {
after,
name,
genders,
education_levels,
pref_gender,
pref_age_min,
pref_age_max,
@@ -82,6 +84,7 @@ export const loadProfiles = async (props: profileQueryType) => {
(l) =>
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
(!genders || genders.includes(l.gender)) &&
(!education_levels || education_levels.includes(l.education_level ?? '')) &&
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
(!pref_age_max || (l.age ?? MIN_INT) <= pref_age_max) &&
@@ -147,7 +150,9 @@ export const loadProfiles = async (props: profileQueryType) => {
{word}
)),
genders?.length && where(`gender = ANY($(gender))`, {gender: genders}),
genders?.length && where(`gender = ANY($(genders))`, {genders}),
education_levels?.length && where(`education_level = ANY($(education_levels))`, {education_levels}),
pref_gender?.length &&
where(`pref_gender is NULL or pref_gender = '{}' OR pref_gender && $(pref_gender)`, {pref_gender}),

View File

@@ -345,6 +345,7 @@ export const API = (_apiTypeCheck = {
// Search and filter parameters
name: z.string().optional(),
genders: arraybeSchema.optional(),
education_levels: arraybeSchema.optional(),
pref_gender: arraybeSchema.optional(),
pref_age_min: z.coerce.number().optional(),
pref_age_max: z.coerce.number().optional(),

View File

@@ -1,5 +1,5 @@
export const MAX_INT = 99999
export const MIN_INT = -MAX_INT
export const MIN_INT = Number.MIN_SAFE_INTEGER
export const MAX_INT = Number.MAX_SAFE_INTEGER
export const supportEmail = 'hello@compassmeet.com';
// export const marketingEmail = 'hello@compassmeet.com';

View File

@@ -15,6 +15,7 @@ export type FilterFields = {
lon: number | null
radius: number | null
genders: string[]
education_levels: string[]
name: string | undefined
shortBio: boolean | undefined
} & Pick<
@@ -57,6 +58,7 @@ export const initialFilters: Partial<FilterFields> = {
radius: undefined,
name: undefined,
genders: undefined,
education_levels: undefined,
pref_age_max: undefined,
pref_age_min: undefined,
has_kids: undefined,

View File

@@ -8,6 +8,7 @@ const filterLabels: Record<string, string> = {
location: "",
name: "Searching",
genders: "",
education_levels: "Education",
pref_age_max: "Max age",
pref_age_min: "Min age",
has_kids: "",

View File

@@ -37,6 +37,15 @@ export const DIET_CHOICES = {
Other: 'other',
}
export const EDUCATION_CHOICES = {
None: 'none',
'High school': 'high-school',
'Some college': 'some-college',
Bachelors: 'bachelors',
Masters: 'masters',
PhD: 'doctorate',
}
export const REVERTED_RELATIONSHIP_CHOICES = Object.fromEntries(
Object.entries(RELATIONSHIP_CHOICES).map(([key, value]) => [value, key])
);
@@ -51,4 +60,8 @@ export const REVERTED_POLITICAL_CHOICES = Object.fromEntries(
export const REVERTED_DIET_CHOICES = Object.fromEntries(
Object.entries(DIET_CHOICES).map(([key, value]) => [value, key])
);
export const REVERTED_EDUCATION_CHOICES = Object.fromEntries(
Object.entries(EDUCATION_CHOICES).map(([key, value]) => [value, key])
);

View File

@@ -25,6 +25,8 @@ import {DietFilter, DietFilterText} from "web/components/filters/diet-filter";
import {PoliticalFilter, PoliticalFilterText} from "web/components/filters/political-filter";
import {GiFruitBowl} from "react-icons/gi";
import {RiScales3Line} from "react-icons/ri";
import {EducationFilter, EducationFilterText} from "web/components/filters/education-filter";
import {LuGraduationCap} from "react-icons/lu";
export function DesktopFilters(props: {
filters: Partial<FilterFields>
@@ -349,6 +351,30 @@ export function DesktopFilters(props: {
menuWidth="w-50"
/>
{/* EDUCATION */}
<CustomizeableDropdown
buttonContent={(open: boolean) => (
<DropdownButton
content={
<Row className="items-center gap-1">
<LuGraduationCap className="h-4 w-4"/>
<EducationFilterText
options={filters.education_levels as string[]}
highlightedClass={open ? 'text-primary-500' : undefined}
/>
</Row>
}
open={open}
/>
)}
dropdownMenuContent={
<Col>
<EducationFilter filters={filters} updateFilter={updateFilter}/>
</Col>
}
popoverClassName="bg-canvas-50"
/>
{/* Short Bios */}
<ShortBioToggle
updateFilter={updateFilter}

View File

@@ -0,0 +1,65 @@
import clsx from 'clsx'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {FilterFields} from "common/filters";
import {EDUCATION_CHOICES} from "web/components/filters/choices";
import {convertEducationTypes} from "web/lib/util/convert-types";
import stringOrStringArrayToText from "web/lib/util/string-or-string-array-to-text";
import {MAX_INT} from "common/constants";
export function EducationFilterText(props: {
options: string[] | undefined
highlightedClass?: string
}) {
const {options, highlightedClass} = props
const length = (options ?? []).length
if (!options || length < 1) {
return (
<span className={clsx('text-semibold', highlightedClass)}>Any education</span>
)
}
const order = Object.values(EDUCATION_CHOICES)
const sortedOptions = options
.slice()
.sort((a, b) => {
const ia = order.indexOf(a as any)
const ib = order.indexOf(b as any)
const sa = ia === -1 ? MAX_INT : ia
const sb = ib === -1 ? MAX_INT : ib
if (sa !== sb) return sa - sb
return String(a).localeCompare(String(b))
})
const convertedTypes = sortedOptions.map((r) => convertEducationTypes(r as any))
return (
<div>
<span className={clsx('font-semibold', highlightedClass)}>
{stringOrStringArrayToText({
text: convertedTypes,
capitalizeFirstLetterOption: true,
})}{' '}
</span>
</div>
)
}
export function EducationFilter(props: {
filters: Partial<FilterFields>
updateFilter: (newState: Partial<FilterFields>) => void
}) {
const {filters, updateFilter} = props
return (
<>
<MultiCheckbox
selected={filters.education_levels ?? []}
choices={EDUCATION_CHOICES}
onChange={(c) => {
updateFilter({education_levels: c})
}}
/>
</>
)
}

View File

@@ -14,14 +14,14 @@ import {DietType, PoliticalType, RelationshipType, RomanticType} from 'web/lib/u
import {FilterFields} from "common/filters";
import {ShortBioToggle} from "web/components/filters/short-bio-toggle";
import {PrefGenderFilter, PrefGenderFilterText} from "./pref-gender-filter"
import {KidsLabel, WantsKidsFilter, WantsKidsIcon} from "web/components/filters/wants-kids-filter";
import {KidsLabel, WantsKidsFilter} from "web/components/filters/wants-kids-filter";
import {wantsKidsLabels} from "common/wants-kids";
import {FaChild} from "react-icons/fa"
import {HasKidsFilter, HasKidsLabel} from "./has-kids-filter"
import {hasKidsLabels} from "common/has-kids";
import {RomanticFilter, RomanticFilterText} from "web/components/filters/romantic-filter";
import {DietFilter, DietFilterText} from "web/components/filters/diet-filter";
import {PoliticalFilter, PoliticalFilterText} from "web/components/filters/political-filter";
import {EducationFilter, EducationFilterText} from "web/components/filters/education-filter";
function MobileFilters(props: {
filters: Partial<FilterFields>
@@ -198,7 +198,7 @@ function MobileFilters(props: {
filters.wants_kids_strength != null &&
filters.wants_kids_strength !== -1
}
// icon={<WantsKidsIcon strength={filters.wants_kids_strength ?? -1}/>}
// icon={<WantsKidsIcon strength={filters.wants_kids_strength ?? -1}/>}
selection={
<KidsLabel
strength={filters.wants_kids_strength ?? -1}
@@ -221,7 +221,7 @@ function MobileFilters(props: {
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={filters.has_kids != null && filters.has_kids !== -1}
// icon={<FaChild className="text-ink-900 h-4 w-4"/>}
// icon={<FaChild className="text-ink-900 h-4 w-4"/>}
selection={
<HasKidsLabel
has_kids={filters.has_kids ?? -1}
@@ -279,6 +279,24 @@ function MobileFilters(props: {
<PoliticalFilter filters={filters} updateFilter={updateFilter}/>
</MobileFilterSection>
{/* EDUCATION */}
<MobileFilterSection
title="Education"
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={hasAny(filters.education_levels)}
selection={
<EducationFilterText
options={filters.education_levels as string[]}
highlightedClass={
hasAny(filters.education_levels) ? 'text-primary-600' : 'text-ink-900'
}
/>
}
>
<EducationFilter filters={filters} updateFilter={updateFilter}/>
</MobileFilterSection>
{/* Short Bios */}
<Col className="p-4 pb-2">
<ShortBioToggle

View File

@@ -78,8 +78,9 @@ export const useFilters = (you: Profile | undefined) => {
}
const yourFilters: Partial<FilterFields> = {
genders: you?.pref_gender?.length ? you.pref_gender : undefined,
pref_gender: you?.gender?.length ? [you.gender] : undefined,
genders: you?.pref_gender?.length ? you.pref_gender : undefined,
education_levels: you?.education_level ? [you.education_level] : undefined,
pref_age_max: (you?.pref_age_max ?? MAX_INT) < 100 ? you?.pref_age_max : undefined,
pref_age_min: (you?.pref_age_min ?? MIN_INT) > 18 ? you?.pref_age_min : undefined,
pref_relation_styles: you?.pref_relation_styles.length ? you.pref_relation_styles : undefined,
@@ -99,6 +100,7 @@ export const useFilters = (you: Profile | undefined) => {
!!you
&& (!location || location.id === you.geodb_city_id)
&& isEqual(filters.genders?.length ? filters.genders : undefined, yourFilters.genders?.length ? yourFilters.genders : undefined)
&& isEqual(new Set(filters.education_levels), new Set([you.education_level]))
&& (!you.gender || filters.pref_gender?.length == 1 && isEqual(filters.pref_gender?.length ? filters.pref_gender[0] : undefined, you.gender))
&& isEqual(new Set(filters.pref_romantic_styles), new Set(you.pref_romantic_styles))
&& isEqual(new Set(filters.pref_relation_styles), new Set(you.pref_relation_styles))

View File

@@ -28,7 +28,13 @@ import {City, CityRow, profileToCity, useCitySearch} from "web/components/search
import {AddPhotosWidget} from './widgets/add-photos'
import {RadioToggleGroup} from "web/components/widgets/radio-toggle-group";
import {MultipleChoiceOptions} from "common/profiles/multiple-choice";
import {DIET_CHOICES, POLITICAL_CHOICES, RELATIONSHIP_CHOICES, ROMANTIC_CHOICES} from "web/components/filters/choices";
import {
DIET_CHOICES,
EDUCATION_CHOICES,
POLITICAL_CHOICES,
RELATIONSHIP_CHOICES,
ROMANTIC_CHOICES
} from "web/components/filters/choices";
import toast from "react-hot-toast";
export const OptionalProfileUserForm = (props: {
@@ -537,14 +543,7 @@ export const OptionalProfileUserForm = (props: {
<Carousel className="max-w-full">
<ChoicesToggleGroup
currentChoice={profile['education_level'] ?? ''}
choicesMap={{
None: 'none',
'High school': 'high-school',
'Some college': 'some-college',
Bachelors: 'bachelors',
Masters: 'masters',
PhD: 'doctorate',
}}
choicesMap={EDUCATION_CHOICES}
setChoice={(c) => setProfile('education_level', c)}
/>
</Carousel>

View File

@@ -3,7 +3,7 @@ import {convertRelationshipType, type RelationshipType,} from 'web/lib/util/conv
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {ReactNode} from 'react'
import {
REVERTED_DIET_CHOICES,
REVERTED_DIET_CHOICES, REVERTED_EDUCATION_CHOICES,
REVERTED_POLITICAL_CHOICES,
REVERTED_ROMANTIC_CHOICES
} from 'web/components/filters/choices'
@@ -170,21 +170,22 @@ function Education(props: { profile: Profile }) {
const educationLevel = profile.education_level
const university = profile.university
const noUniversity =
!educationLevel ||
educationLevel == 'high-school' ||
educationLevel == 'none'
let text = ''
if (!university || noUniversity) {
if (educationLevel) {
text += capitalizeAndRemoveUnderscores(REVERTED_EDUCATION_CHOICES[educationLevel])
}
if (university) {
if (educationLevel) text += ' at '
text += capitalizeAndRemoveUnderscores(university)
}
if (text.length === 0) {
return <></>
}
const universityText = `${
noUniversity ? '' : capitalizeAndRemoveUnderscores(educationLevel) + ' at '
}${capitalizeAndRemoveUnderscores(university)}`
return (
<AboutRow
icon={<LuGraduationCap className="h-5 w-5"/>}
text={universityText}
text={text}
/>
)
}

View File

@@ -1,14 +1,16 @@
import {
REVERTED_DIET_CHOICES,
REVERTED_EDUCATION_CHOICES,
REVERTED_POLITICAL_CHOICES,
REVERTED_RELATIONSHIP_CHOICES,
REVERTED_ROMANTIC_CHOICES,
REVERTED_POLITICAL_CHOICES
REVERTED_ROMANTIC_CHOICES
} from "web/components/filters/choices";
export type RelationshipType = keyof typeof REVERTED_RELATIONSHIP_CHOICES
export type RomanticType = keyof typeof REVERTED_ROMANTIC_CHOICES
export type DietType = keyof typeof REVERTED_DIET_CHOICES
export type PoliticalType = keyof typeof REVERTED_POLITICAL_CHOICES
export type EducationType = keyof typeof REVERTED_EDUCATION_CHOICES
export function convertRelationshipType(relationshipType: RelationshipType) {
return REVERTED_RELATIONSHIP_CHOICES[relationshipType]
@@ -25,3 +27,7 @@ export function convertDietTypes(dietType: DietType) {
export function convertPoliticalTypes(politicalType: PoliticalType) {
return REVERTED_POLITICAL_CHOICES[politicalType]
}
export function convertEducationTypes(educationType: EducationType) {
return REVERTED_EDUCATION_CHOICES[educationType]
}