mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-08 16:48:50 -04:00
Add filter for education level
This commit is contained in:
@@ -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}),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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])
|
||||
);
|
||||
@@ -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}
|
||||
|
||||
65
web/components/filters/education-filter.tsx
Normal file
65
web/components/filters/education-filter.tsx
Normal 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})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user