From 9d649daee5e74c20ee7952c76623272ebcb1d3a1 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 13 Nov 2025 15:12:11 +0100 Subject: [PATCH] Add profile field and filter: MBTI --- backend/api/src/get-profiles.ts | 5 ++ backend/email/emails/functions/mock.ts | 2 + .../20250101120000_add_mbti_to_profiles.sql | 6 ++ common/src/api/schema.ts | 1 + common/src/api/zod-types.ts | 1 + common/src/filters.ts | 2 + common/src/searches.ts | 1 + common/src/supabase/schema.ts | 68 +++++++++++-------- web/components/filters/choices.tsx | 30 ++++++-- web/components/filters/desktop-filters.tsx | 25 +++++++ web/components/filters/mbti-filter.tsx | 61 +++++++++++++++++ web/components/filters/mobile-filters.tsx | 19 ++++++ web/components/filters/use-filters.ts | 2 + web/components/optional-profile-form.tsx | 14 +++- web/components/profile-about.tsx | 7 +- 15 files changed, 207 insertions(+), 37 deletions(-) create mode 100644 backend/supabase/migrations/20250101120000_add_mbti_to_profiles.sql create mode 100644 web/components/filters/mbti-filter.tsx diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index 61ee337a..56453197 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -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}), diff --git a/backend/email/emails/functions/mock.ts b/backend/email/emails/functions/mock.ts index 0c3898d1..379ec8ef 100644 --- a/backend/email/emails/functions/mock.ts +++ b/backend/email/emails/functions/mock.ts @@ -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', diff --git a/backend/supabase/migrations/20250101120000_add_mbti_to_profiles.sql b/backend/supabase/migrations/20250101120000_add_mbti_to_profiles.sql new file mode 100644 index 00000000..aa796400 --- /dev/null +++ b/backend/supabase/migrations/20250101120000_add_mbti_to_profiles.sql @@ -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); diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index c4619278..879c84ab 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -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(), diff --git a/common/src/api/zod-types.ts b/common/src/api/zod-types.ts index 37768fa9..ef1fc092 100644 --- a/common/src/api/zod-types.ts +++ b/common/src/api/zod-types.ts @@ -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(), diff --git a/common/src/filters.ts b/common/src/filters.ts index 50507e02..6d3c48b6 100644 --- a/common/src/filters.ts +++ b/common/src/filters.ts @@ -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 = { relationship_status: undefined, languages: undefined, religion: undefined, + mbti: undefined, pref_gender: undefined, shortBio: undefined, drinks_min: undefined, diff --git a/common/src/searches.ts b/common/src/searches.ts index ba33f0a0..8ca3d5d1 100644 --- a/common/src/searches.ts +++ b/common/src/searches.ts @@ -23,6 +23,7 @@ const filterLabels: Record = { diet: "Diet", political_beliefs: "Political views", languages: "", + mbti: "MBTI", } export type locationType = { diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index 1d4215dd..da5e26d8 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -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 type DefaultSchema = DatabaseWithoutInternals[Extract] 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] diff --git a/web/components/filters/choices.tsx b/web/components/filters/choices.tsx index 79e61d00..375bda18 100644 --- a/web/components/filters/choices.tsx +++ b/web/components/filters/choices.tsx @@ -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) \ No newline at end of file +export const INVERTED_RACE_CHOICES = invert(RACE_CHOICES) +export const INVERTED_MBTI_CHOICES = invert(MBTI_CHOICES) \ No newline at end of file diff --git a/web/components/filters/desktop-filters.tsx b/web/components/filters/desktop-filters.tsx index db5493cd..c92a865e 100644 --- a/web/components/filters/desktop-filters.tsx +++ b/web/components/filters/desktop-filters.tsx @@ -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 */} + ( + + + + + } + /> + )} + dropdownMenuContent={ + + } + popoverClassName="bg-canvas-50" + menuWidth="w-[350px]" + /> + {/* SMOKER */} ( diff --git a/web/components/filters/mbti-filter.tsx b/web/components/filters/mbti-filter.tsx new file mode 100644 index 00000000..992f44cb --- /dev/null +++ b/web/components/filters/mbti-filter.tsx @@ -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 ( + Any MBTI + ) + } + + if (length > 2) { + return ( + + + Multiple + + + ) + } + + const sortedOptions = getSortedOptions(options, MBTI_CHOICES) + const displayTypes = sortedOptions.map((type) => type.toUpperCase()) + + return ( +
+ + {stringOrStringArrayToText({ + text: displayTypes, + capitalizeFirstLetterOption: false, + })}{' '} + +
+ ) +} + +export function MbtiFilter(props: { + filters: Partial + updateFilter: (newState: Partial) => void +}) { + const {filters, updateFilter} = props + + return ( + { + updateFilter({mbti: c}) + }} + /> + ) +} diff --git a/web/components/filters/mobile-filters.tsx b/web/components/filters/mobile-filters.tsx index 1a11c43a..b6e5bac7 100644 --- a/web/components/filters/mobile-filters.tsx +++ b/web/components/filters/mobile-filters.tsx @@ -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 @@ -401,6 +402,24 @@ function MobileFilters(props: { + {/* MBTI */} + + } + > + + + {/* EDUCATION */} { 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)) diff --git a/web/components/optional-profile-form.tsx b/web/components/optional-profile-form.tsx index 133c664f..8c251c46 100644 --- a/web/components/optional-profile-form.tsx +++ b/web/components/optional-profile-form.tsx @@ -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: { /> + + + setProfile('mbti', c)} + className="grid grid-cols-4 xs:grid-cols-8" + /> + + } text={profile.diet?.map(e => INVERTED_DIET_CHOICES[e])} /> + } + text={profile.mbti ? INVERTED_MBTI_CHOICES[profile.mbti] : null} + /> {!isCurrentUser && }