From b94cdba5af377b06c31cebb97c0a772ad6324690 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 23 Oct 2025 14:58:57 +0200 Subject: [PATCH] Add diet --- backend/api/src/get-profiles.ts | 10 +++ backend/email/emails/functions/mock.ts | 4 +- backend/supabase/profiles.sql | 2 +- common/src/api/schema.ts | 1 + common/src/api/zod-types.ts | 2 +- common/src/filters.ts | 2 + common/src/supabase/schema.ts | 6 +- web/components/filters/choices.tsx | 14 +++++ web/components/filters/desktop-filters.tsx | 31 +++++++++- web/components/filters/diet-filter.tsx | 61 +++++++++++++++++++ web/components/filters/mobile-filters.tsx | 23 ++++++- .../filters/relationship-filter.tsx | 2 +- web/components/filters/romantic-filter.tsx | 2 +- web/components/filters/use-filters.ts | 2 + web/components/optional-profile-form.tsx | 11 +++- web/components/profile-about.tsx | 17 ++++-- web/components/profiles/profiles-home.tsx | 6 +- ...-relationship-type.ts => convert-types.ts} | 11 +++- 18 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 web/components/filters/diet-filter.tsx rename web/lib/util/{convert-relationship-type.ts => convert-types.ts} (63%) diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index 1e0a31c1..378f7767 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -17,6 +17,7 @@ export type profileQueryType = { pref_age_max?: number | undefined, pref_relation_styles?: String[] | undefined, pref_romantic_styles?: String[] | undefined, + diet?: String[] | undefined, wants_kids_strength?: number | undefined, has_kids?: number | undefined, is_smoker?: boolean | undefined, @@ -47,6 +48,7 @@ export const loadProfiles = async (props: profileQueryType) => { pref_age_max, pref_relation_styles, pref_romantic_styles, + diet, wants_kids_strength, has_kids, is_smoker, @@ -85,6 +87,8 @@ export const loadProfiles = async (props: profileQueryType) => { intersection(pref_relation_styles, l.pref_relation_styles).length) && (!pref_romantic_styles || intersection(pref_romantic_styles, l.pref_romantic_styles).length) && + (!diet || + intersection(diet, l.diet).length) && (!wants_kids_strength || wants_kids_strength == -1 || !l.wants_kids_strength || @@ -162,6 +166,12 @@ export const loadProfiles = async (props: profileQueryType) => { {pref_romantic_styles} ), + diet?.length && + where( + `diet IS NULL OR diet = '{}' OR diet && $(diet)`, + {diet} + ), + !!wants_kids_strength && wants_kids_strength !== -1 && where( diff --git a/backend/email/emails/functions/mock.ts b/backend/email/emails/functions/mock.ts index 46335960..d5528a1b 100644 --- a/backend/email/emails/functions/mock.ts +++ b/backend/email/emails/functions/mock.ts @@ -46,7 +46,7 @@ export const sinclairProfile: ProfileRow = { has_kids: 0, is_smoker: false, drinks_per_month: 0, - is_vegetarian_or_vegan: null, + diet: null, political_beliefs: ['e/acc', 'libertarian'], religious_belief_strength: null, religious_beliefs: null, @@ -147,7 +147,7 @@ export const jamesProfile: ProfileRow = { has_kids: 0, is_smoker: false, drinks_per_month: 5, - is_vegetarian_or_vegan: null, + diet: null, political_beliefs: ['libertarian'], religious_belief_strength: null, religious_beliefs: '', diff --git a/backend/supabase/profiles.sql b/backend/supabase/profiles.sql index e7327b0b..d366806b 100644 --- a/backend/supabase/profiles.sql +++ b/backend/supabase/profiles.sql @@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS profiles ( height_in_inches INTEGER, id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL, is_smoker BOOLEAN, - is_vegetarian_or_vegan BOOLEAN, + diet TEXT[], last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL, looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL, messaging_status TEXT DEFAULT 'open'::TEXT NOT NULL, diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 7543c589..d158f236 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -350,6 +350,7 @@ export const API = (_apiTypeCheck = { pref_age_max: z.coerce.number().optional(), pref_relation_styles: arraybeSchema.optional(), pref_romantic_styles: arraybeSchema.optional(), + diet: arraybeSchema.optional(), wants_kids_strength: z.coerce.number().optional(), has_kids: z.coerce.number().optional(), is_smoker: z.coerce.boolean().optional(), diff --git a/common/src/api/zod-types.ts b/common/src/api/zod-types.ts index edecd9ba..16bfa405 100644 --- a/common/src/api/zod-types.ts +++ b/common/src/api/zod-types.ts @@ -80,7 +80,7 @@ const optionalProfilesSchema = z.object({ education_level: z.string().optional(), is_smoker: z.boolean().optional(), drinks_per_month: z.number().min(0).optional(), - is_vegetarian_or_vegan: z.boolean().optional(), + diet: z.array(z.string()).optional(), has_kids: z.number().min(0).optional(), university: z.string().optional(), occupation_title: z.string().optional(), diff --git a/common/src/filters.ts b/common/src/filters.ts index 45f0c248..f57af4b1 100644 --- a/common/src/filters.ts +++ b/common/src/filters.ts @@ -22,6 +22,7 @@ export type FilterFields = { | 'wants_kids_strength' | 'pref_relation_styles' | 'pref_romantic_styles' + | 'diet' | 'is_smoker' | 'has_kids' | 'pref_gender' @@ -62,6 +63,7 @@ export const initialFilters: Partial = { is_smoker: undefined, pref_relation_styles: undefined, pref_romantic_styles: undefined, + diet: undefined, pref_gender: undefined, shortBio: undefined, orderBy: 'created_time', diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index 27c5ef97..ea3de999 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -454,7 +454,7 @@ export type Database = { height_in_inches: number | null id: number is_smoker: boolean | null - is_vegetarian_or_vegan: boolean | null + diet: string[] | null last_modification_time: string looking_for_matches: boolean messaging_status: string @@ -502,7 +502,7 @@ export type Database = { height_in_inches?: number | null id?: number is_smoker?: boolean | null - is_vegetarian_or_vegan?: boolean | null + diet?: string[] | null last_modification_time?: string looking_for_matches?: boolean messaging_status?: string @@ -550,7 +550,7 @@ export type Database = { height_in_inches?: number | null id?: number is_smoker?: boolean | null - is_vegetarian_or_vegan?: boolean | null + diet?: string[] | null last_modification_time?: string looking_for_matches?: boolean messaging_status?: string diff --git a/web/components/filters/choices.tsx b/web/components/filters/choices.tsx index 6810e3cf..e68df45b 100644 --- a/web/components/filters/choices.tsx +++ b/web/components/filters/choices.tsx @@ -27,6 +27,16 @@ export const POLITICAL_CHOICES = { 'Independent / Other': 'other', } +export const DIET_CHOICES = { + Omnivore: 'omnivore', + Vegetarian: 'veg', + Vegan: 'vegan', + Keto: 'keto', + Paleo: 'paleo', + Pescetarian: 'pescetarian', + Other: 'other', +} + export const REVERTED_RELATIONSHIP_CHOICES = Object.fromEntries( Object.entries(RELATIONSHIP_CHOICES).map(([key, value]) => [value, key]) ); @@ -37,4 +47,8 @@ export const REVERTED_ROMANTIC_CHOICES = Object.fromEntries( export const REVERTED_POLITICAL_CHOICES = Object.fromEntries( Object.entries(POLITICAL_CHOICES).map(([key, value]) => [value, key]) +); + +export const REVERTED_DIET_CHOICES = Object.fromEntries( + Object.entries(DIET_CHOICES).map(([key, value]) => [value, key]) ); \ No newline at end of file diff --git a/web/components/filters/desktop-filters.tsx b/web/components/filters/desktop-filters.tsx index 2ec04b36..efda8729 100644 --- a/web/components/filters/desktop-filters.tsx +++ b/web/components/filters/desktop-filters.tsx @@ -1,5 +1,5 @@ import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/outline' -import {RelationshipType, RomanticType} from 'web/lib/util/convert-relationship-type' +import {DietType, RelationshipType, RomanticType} from 'web/lib/util/convert-types' import {ReactNode} from 'react' import {FaUserGroup} from 'react-icons/fa6' import {Col} from 'web/components/layout/col' @@ -21,6 +21,7 @@ import {hasKidsLabels} from "common/has-kids"; import {HasKidsLabel} from "web/components/filters/has-kids-filter"; import {RomanticFilter, RomanticFilterText} from "web/components/filters/romantic-filter"; import {FaHeart} from "react-icons/fa"; +import {DietFilter, DietFilterText} from "web/components/filters/diet-filter"; export function DesktopFilters(props: { filters: Partial @@ -171,7 +172,7 @@ export function DesktopFilters(props: { {includeRelationshipFilters && <> - {/* CONNECTION */} + {/* RELATIONSHIP STYLE */} ( } + {/* DIET */} + ( + + + + } + /> + )} + dropdownMenuContent={ + + } + popoverClassName="bg-canvas-50" + menuWidth="w-50" + /> + {/* Short Bios */} Any diet + ) + } + + const convertedTypes = options.map((r) => + convertDietTypes(r) + ) + + if (length > 1) { + return ( + + + Multiple + + + ) + } + return ( +
+ + {stringOrStringArrayToText({ + text: convertedTypes, + capitalizeFirstLetterOption: true, + })}{' '} + +
+ ) +} + +export function DietFilter(props: { + filters: Partial + updateFilter: (newState: Partial) => void +}) { + const {filters, updateFilter} = props + return ( + { + updateFilter({diet: c}) + }} + /> + ) +} diff --git a/web/components/filters/mobile-filters.tsx b/web/components/filters/mobile-filters.tsx index a9c09e10..ca30580a 100644 --- a/web/components/filters/mobile-filters.tsx +++ b/web/components/filters/mobile-filters.tsx @@ -10,7 +10,7 @@ import {RelationshipFilter, RelationshipFilterText,} from './relationship-filter import {MyMatchesToggle} from './my-matches-toggle' import {Profile} from 'common/profiles/profile' import {Gender} from 'common/gender' -import {RelationshipType, RomanticType} from 'web/lib/util/convert-relationship-type' +import {DietType, RelationshipType, RomanticType} from 'web/lib/util/convert-types' import {FilterFields} from "common/filters"; import {ShortBioToggle} from "web/components/filters/short-bio-toggle"; import {PrefGenderFilter, PrefGenderFilterText} from "./pref-gender-filter" @@ -20,6 +20,7 @@ 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"; function MobileFilters(props: { filters: Partial @@ -237,6 +238,26 @@ function MobileFilters(props: { } + {/* DIET */} + + } + > + + + {/* Short Bios */} { 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, pref_romantic_styles: you?.pref_romantic_styles?.length ? you.pref_romantic_styles : undefined, + diet: you?.diet?.length ? you.diet : undefined, wants_kids_strength: wantsKidsDatabaseToWantsKidsFilter( (you?.wants_kids_strength ?? 2) as wantsKidsDatabase ), @@ -100,6 +101,7 @@ export const useFilters = (you: Profile | undefined) => { 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)) && + isEqual(new Set(filters.diet), new Set(you.diet)) && filters.pref_age_max == yourFilters.pref_age_max && filters.pref_age_min == yourFilters.pref_age_min && filters.wants_kids_strength == yourFilters.wants_kids_strength diff --git a/web/components/optional-profile-form.tsx b/web/components/optional-profile-form.tsx index edda87a5..6d864f00 100644 --- a/web/components/optional-profile-form.tsx +++ b/web/components/optional-profile-form.tsx @@ -28,7 +28,7 @@ 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 {POLITICAL_CHOICES, RELATIONSHIP_CHOICES, ROMANTIC_CHOICES} from "web/components/filters/choices"; +import {DIET_CHOICES, POLITICAL_CHOICES, RELATIONSHIP_CHOICES, ROMANTIC_CHOICES} from "web/components/filters/choices"; import toast from "react-hot-toast"; export const OptionalProfileUserForm = (props: { @@ -409,6 +409,15 @@ export const OptionalProfileUserForm = (props: { /> + + + setProfile('diet', selected)} + /> + + } - text={profile.is_vegetarian_or_vegan ? 'Vegetarian/Vegan' : null} + icon={} + text={profile.diet?.map(e => REVERTED_DIET_CHOICES[e])} /> @@ -296,7 +301,7 @@ export const formatProfileValue = (key: string, value: any) => { case 'last_online_time': return fromNow(new Date(value).valueOf()) case 'is_smoker': - case 'is_vegetarian_or_vegan': + case 'diet': case 'has_pets': return value ? 'Yes' : 'No' case 'height_in_inches': diff --git a/web/components/profiles/profiles-home.tsx b/web/components/profiles/profiles-home.tsx index 9ca479cb..62404e02 100644 --- a/web/components/profiles/profiles-home.tsx +++ b/web/components/profiles/profiles-home.tsx @@ -50,11 +50,13 @@ export function ProfilesHome() { if (!user) return; setIsReloading(true); const current = ++id.current; - api('get-profiles', removeNullOrUndefinedProps({ + const args = removeNullOrUndefinedProps({ limit: 20, compatibleWithUserId: user?.id, ...filters - }) as any) + }); + console.debug('Refreshing profiles, filters:', args); + api('get-profiles', args as any) .then(({profiles}) => { if (current === id.current) setProfiles(profiles); }) diff --git a/web/lib/util/convert-relationship-type.ts b/web/lib/util/convert-types.ts similarity index 63% rename from web/lib/util/convert-relationship-type.ts rename to web/lib/util/convert-types.ts index 51abd0a9..b0f8939c 100644 --- a/web/lib/util/convert-relationship-type.ts +++ b/web/lib/util/convert-types.ts @@ -1,7 +1,12 @@ -import {REVERTED_RELATIONSHIP_CHOICES, REVERTED_ROMANTIC_CHOICES} from "web/components/filters/choices"; +import { + REVERTED_DIET_CHOICES, + REVERTED_RELATIONSHIP_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 function convertRelationshipType(relationshipType: RelationshipType) { return REVERTED_RELATIONSHIP_CHOICES[relationshipType] @@ -10,3 +15,7 @@ export function convertRelationshipType(relationshipType: RelationshipType) { export function convertRomanticTypes(romanticType: RomanticType) { return REVERTED_ROMANTIC_CHOICES[romanticType] } + +export function convertDietTypes(dietType: DietType) { + return REVERTED_DIET_CHOICES[dietType] +}