Add profile field and filter: MBTI

This commit is contained in:
MartinBraquet
2025-11-13 15:12:11 +01:00
parent 7598a47283
commit 9d649daee5
15 changed files with 207 additions and 37 deletions

View File

@@ -22,6 +22,7 @@ export type profileQueryType = {
pref_romantic_styles?: String[] | undefined,
diet?: String[] | undefined,
political_beliefs?: String[] | undefined,
mbti?: String[] | undefined,
relationship_status?: String[] | undefined,
languages?: String[] | undefined,
religion?: String[] | undefined,
@@ -60,6 +61,7 @@ export const loadProfiles = async (props: profileQueryType) => {
pref_romantic_styles,
diet,
political_beliefs,
mbti,
relationship_status,
languages,
religion,
@@ -95,6 +97,7 @@ export const loadProfiles = async (props: profileQueryType) => {
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
(!genders || genders.includes(l.gender ?? '')) &&
(!education_levels || education_levels.includes(l.education_level ?? '')) &&
(!mbti || mbti.includes(l.mbti ?? '')) &&
(!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) &&
@@ -174,6 +177,8 @@ export const loadProfiles = async (props: profileQueryType) => {
education_levels?.length && where(`education_level = ANY($(education_levels))`, {education_levels}),
mbti?.length && where(`mbti = ANY($(mbti))`, {mbti}),
pref_gender?.length &&
where(`pref_gender is NULL or pref_gender = '{}' OR pref_gender && $(pref_gender)`, {pref_gender}),

View File

@@ -44,6 +44,7 @@ export const sinclairProfile: ProfileRow = {
wants_kids_strength: 3,
looking_for_matches: true,
visibility: 'public',
mbti: 'intj',
messaging_status: 'open',
comments_enabled: true,
has_kids: 0,
@@ -160,6 +161,7 @@ export const jamesProfile: ProfileRow = {
relationship_status: ['single'],
religious_belief_strength: null,
religious_beliefs: '',
mbti: 'intj',
political_details: '',
photo_urls: [
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2FKl0WtbZsZW.jpg?alt=media&token=c928604f-e5ff-4406-a229-152864a4aa48',

View File

@@ -0,0 +1,6 @@
-- Add MBTI column to profiles table
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS mbti TEXT;
-- Create GIN index for array operations
CREATE INDEX IF NOT EXISTS idx_profiles_mbti ON profiles USING btree (mbti);

View File

@@ -405,6 +405,7 @@ export const API = (_apiTypeCheck = {
pref_romantic_styles: arraybeSchema.optional(),
diet: arraybeSchema.optional(),
political_beliefs: arraybeSchema.optional(),
mbti: arraybeSchema.optional(),
relationship_status: arraybeSchema.optional(),
languages: arraybeSchema.optional(),
wants_kids_strength: z.coerce.number().optional(),

View File

@@ -82,6 +82,7 @@ const optionalProfilesSchema = z.object({
drinks_min: z.number().min(0).optional().nullable(),
drinks_per_month: z.number().min(0).optional().nullable(),
education_level: z.string().optional().nullable(),
mbti: z.string().optional().nullable(),
ethnicity: z.array(z.string()).optional().nullable(),
has_kids: z.number().min(0).optional().nullable(),
has_pets: zBoolean.optional().nullable(),

View File

@@ -16,6 +16,7 @@ export type FilterFields = {
radius: number | null
genders: string[]
education_levels: string[]
mbti: string[]
name: string | undefined
shortBio: boolean | undefined
drinks_min: number | undefined
@@ -76,6 +77,7 @@ export const initialFilters: Partial<FilterFields> = {
relationship_status: undefined,
languages: undefined,
religion: undefined,
mbti: undefined,
pref_gender: undefined,
shortBio: undefined,
drinks_min: undefined,

View File

@@ -23,6 +23,7 @@ const filterLabels: Record<string, string> = {
diet: "Diet",
political_beliefs: "Political views",
languages: "",
mbti: "MBTI",
}
export type locationType = {

View File

@@ -566,6 +566,7 @@ export type Database = {
languages: string[] | null
last_modification_time: string
looking_for_matches: boolean
mbti: string | null
messaging_status: string
occupation: string | null
occupation_title: string | null
@@ -619,6 +620,7 @@ export type Database = {
languages?: string[] | null
last_modification_time?: string
looking_for_matches?: boolean
mbti?: string | null
messaging_status?: string
occupation?: string | null
occupation_title?: string | null
@@ -672,6 +674,7 @@ export type Database = {
languages?: string[] | null
last_modification_time?: string
looking_for_matches?: boolean
mbti?: string | null
messaging_status?: string
occupation?: string | null
occupation_title?: string | null
@@ -1099,27 +1102,28 @@ type DatabaseWithoutInternals = Omit<Database, '__InternalSupabase'>
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, 'public'>]
export type Tables<
DefaultSchemaTableNameOrOptions extends | keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] &
DefaultSchema['Views'])
DefaultSchema['Views'])
? (DefaultSchema['Tables'] &
DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
@@ -1127,16 +1131,17 @@ export type Tables<
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema['Tables']
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables']
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Insert: infer I
}
@@ -1151,16 +1156,17 @@ export type TablesInsert<
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema['Tables']
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables']
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Update: infer U
}
@@ -1175,32 +1181,34 @@ export type TablesUpdate<
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends | keyof DefaultSchema['Enums']
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema['Enums']
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
: never = never,
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends | keyof DefaultSchema['CompositeTypes']
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema['CompositeTypes']
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
: never = never,
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]

View File

@@ -50,9 +50,9 @@ export const DIET_CHOICES = {
export const EDUCATION_CHOICES = {
'High school': 'high-school',
'College': 'some-college',
Bachelors: 'bachelors',
Masters: 'masters',
PhD: 'doctorate',
'Bachelors': 'bachelors',
'Masters': 'masters',
'PhD': 'doctorate',
}
export const RELIGION_CHOICES = {
@@ -186,7 +186,26 @@ export const RACE_CHOICES = {
'Middle Eastern': 'middle_eastern',
'Native American/Indigenous': 'native_american',
Other: 'other',
} as const
}
export const MBTI_CHOICES = {
'INTJ': 'intj',
'INTP': 'intp',
'INFJ': 'infj',
'INFP': 'infp',
'ISTJ': 'istj',
'ISTP': 'istp',
'ISFJ': 'isfj',
'ISFP': 'isfp',
'ENTJ': 'entj',
'ENTP': 'entp',
'ENFJ': 'enfj',
'ENFP': 'enfp',
'ESTJ': 'estj',
'ESTP': 'estp',
'ESFJ': 'esfj',
'ESFP': 'esfp',
}
export const INVERTED_RELATIONSHIP_CHOICES = invert(RELATIONSHIP_CHOICES)
export const INVERTED_RELATIONSHIP_STATUS_CHOICES = invert(RELATIONSHIP_STATUS_CHOICES)
@@ -196,4 +215,5 @@ export const INVERTED_DIET_CHOICES = invert(DIET_CHOICES)
export const INVERTED_EDUCATION_CHOICES = invert(EDUCATION_CHOICES)
export const INVERTED_RELIGION_CHOICES = invert(RELIGION_CHOICES)
export const INVERTED_LANGUAGE_CHOICES = invert(LANGUAGE_CHOICES)
export const INVERTED_RACE_CHOICES = invert(RACE_CHOICES)
export const INVERTED_RACE_CHOICES = invert(RACE_CHOICES)
export const INVERTED_MBTI_CHOICES = invert(MBTI_CHOICES)

View File

@@ -26,6 +26,8 @@ import {PoliticalFilter, PoliticalFilterText} from "web/components/filters/polit
import {GiFruitBowl} from "react-icons/gi";
import {RiScales3Line} from "react-icons/ri";
import {EducationFilter, EducationFilterText} from "web/components/filters/education-filter";
import {MbtiFilter, MbtiFilterText} from "web/components/filters/mbti-filter";
import {BsPersonVcard} from "react-icons/bs";
import {LuCigarette, LuGraduationCap} from "react-icons/lu";
import {DrinksFilter, DrinksFilterText} from "web/components/filters/drinks-filter";
import {MdLanguage, MdLocalBar} from 'react-icons/md'
@@ -469,6 +471,29 @@ export function DesktopFilters(props: {
menuWidth="w-50"
/>
{/* MBTI */}
<CustomizeableDropdown
buttonContent={(open) => (
<DropdownButton
open={open}
content={
<Row className="items-center gap-1">
<BsPersonVcard className="h-4 w-4" />
<MbtiFilterText
options={filters.mbti as string[] | undefined}
highlightedClass={open ? 'text-primary-500' : undefined}
/>
</Row>
}
/>
)}
dropdownMenuContent={
<MbtiFilter filters={filters} updateFilter={updateFilter} />
}
popoverClassName="bg-canvas-50"
menuWidth="w-[350px]"
/>
{/* SMOKER */}
<CustomizeableDropdown
buttonContent={(open) => (

View File

@@ -0,0 +1,61 @@
import clsx from 'clsx'
import {MBTI_CHOICES} from 'web/components/filters/choices'
import {FilterFields} from 'common/filters'
import {getSortedOptions} from 'common/util/sorting'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
export function MbtiFilterText(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 MBTI</span>
)
}
if (length > 2) {
return (
<span>
<span className={clsx('font-semibold', highlightedClass)}>
Multiple
</span>
</span>
)
}
const sortedOptions = getSortedOptions(options, MBTI_CHOICES)
const displayTypes = sortedOptions.map((type) => type.toUpperCase())
return (
<div>
<span className={clsx('font-semibold', highlightedClass)}>
{stringOrStringArrayToText({
text: displayTypes,
capitalizeFirstLetterOption: false,
})}{' '}
</span>
</div>
)
}
export function MbtiFilter(props: {
filters: Partial<FilterFields>
updateFilter: (newState: Partial<FilterFields>) => void
}) {
const {filters, updateFilter} = props
return (
<MultiCheckbox
selected={filters.mbti ?? []}
choices={MBTI_CHOICES as any}
onChange={(c) => {
updateFilter({mbti: c})
}}
/>
)
}

View File

@@ -30,6 +30,7 @@ import {
RelationshipStatusFilter,
RelationshipStatusFilterText
} from "web/components/filters/relationship-status-filter";
import {MbtiFilter, MbtiFilterText} from "web/components/filters/mbti-filter";
function MobileFilters(props: {
filters: Partial<FilterFields>
@@ -401,6 +402,24 @@ function MobileFilters(props: {
<ReligionFilter filters={filters} updateFilter={updateFilter}/>
</MobileFilterSection>
{/* MBTI */}
<MobileFilterSection
title="MBTI"
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={hasAny(filters.mbti)}
selection={
<MbtiFilterText
options={filters.mbti as string[]}
highlightedClass={
hasAny(filters.mbti) ? 'text-primary-600' : 'text-ink-900'
}
/>
}
>
<MbtiFilter filters={filters} updateFilter={updateFilter}/>
</MobileFilterSection>
{/* EDUCATION */}
<MobileFilterSection
title="Education"

View File

@@ -74,6 +74,7 @@ export const useFilters = (you: Profile | undefined) => {
pref_romantic_styles: you?.pref_romantic_styles?.length ? you.pref_romantic_styles : undefined,
diet: you?.diet?.length ? you.diet : undefined,
political_beliefs: you?.political_beliefs?.length ? you.political_beliefs : undefined,
mbti: you?.mbti ? [you.mbti] : undefined,
relationship_status: you?.relationship_status?.length ? you.relationship_status : undefined,
languages: you?.languages?.length ? you.languages : undefined,
religion: you?.religion?.length ? you.religion : undefined,
@@ -93,6 +94,7 @@ export const useFilters = (you: Profile | undefined) => {
&& isEqual(filters.genders?.length ? filters.genders : undefined, yourFilters.genders?.length ? yourFilters.genders : undefined)
&& (!you.gender && !filters.pref_gender?.length || filters.pref_gender?.length == 1 && isEqual(filters.pref_gender?.length ? filters.pref_gender[0] : undefined, you.gender))
&& (!you.education_level && !filters.education_levels?.length || filters.education_levels?.length == 1 && isEqual(filters.education_levels?.length ? filters.education_levels[0] : undefined, you.education_level))
&& (!you.mbti && !filters.mbti?.length || filters.mbti?.length == 1 && isEqual(filters.mbti?.length ? filters.mbti[0] : undefined, you.mbti))
&& 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))
&& isEqual(new Set(filters.diet), new Set(you.diet))

View File

@@ -31,7 +31,9 @@ import {
DIET_CHOICES,
EDUCATION_CHOICES,
LANGUAGE_CHOICES,
POLITICAL_CHOICES, RACE_CHOICES,
MBTI_CHOICES,
POLITICAL_CHOICES,
RACE_CHOICES,
RELATIONSHIP_CHOICES,
RELATIONSHIP_STATUS_CHOICES,
RELIGION_CHOICES,
@@ -439,6 +441,16 @@ export const OptionalProfileUserForm = (props: {
/>
</Col>
<Col className={clsx(colClassName, 'max-w-[550px]')}>
<label className={clsx(labelClassName)}>MBTI Personality Type</label>
<ChoicesToggleGroup
currentChoice={profile['mbti'] ?? ''}
choicesMap={MBTI_CHOICES}
setChoice={(c) => setProfile('mbti', c)}
className="grid grid-cols-4 xs:grid-cols-8"
/>
</Col>
<Col className={clsx(colClassName)}>
<label className={clsx(labelClassName)}>Diet</label>
<MultiCheckbox

View File

@@ -6,13 +6,14 @@ import {
INVERTED_DIET_CHOICES,
INVERTED_EDUCATION_CHOICES,
INVERTED_LANGUAGE_CHOICES,
INVERTED_MBTI_CHOICES,
INVERTED_POLITICAL_CHOICES,
INVERTED_RELATIONSHIP_STATUS_CHOICES,
INVERTED_RELIGION_CHOICES,
INVERTED_ROMANTIC_CHOICES
} from 'web/components/filters/choices'
import {BiSolidDrink} from 'react-icons/bi'
import {BsPersonHeart} from 'react-icons/bs'
import {BsPersonHeart, BsPersonVcard} from 'react-icons/bs'
import {FaChild} from 'react-icons/fa6'
import {LuBriefcase, LuCigarette, LuCigaretteOff, LuGraduationCap,} from 'react-icons/lu'
import {MdLanguage, MdNoDrinks, MdOutlineChildFriendly} from 'react-icons/md'
@@ -109,6 +110,10 @@ export default function ProfileAbout(props: {
icon={<GiFruitBowl className="h-5 w-5"/>}
text={profile.diet?.map(e => INVERTED_DIET_CHOICES[e])}
/>
<AboutRow
icon={<BsPersonVcard className="h-5 w-5"/>}
text={profile.mbti ? INVERTED_MBTI_CHOICES[profile.mbti] : null}
/>
<HasKids profile={profile}/>
<WantsKids profile={profile}/>
{!isCurrentUser && <LastOnline lastOnlineTime={userActivity?.last_online_time}/>}