Add profile field: place they grew up

This commit is contained in:
MartinBraquet
2026-02-21 19:47:26 +01:00
parent 0277cc390d
commit b6d7955130
25 changed files with 405 additions and 213 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@compass/api",
"description": "Backend API endpoints",
"version": "1.10.1",
"version": "1.11.0",
"private": true,
"scripts": {
"watch:serve": "tsx watch src/serve.ts",

View File

@@ -52,6 +52,9 @@ export type profileQueryType = {
lat?: number | undefined
lon?: number | undefined
radius?: number | undefined
raised_in_lat?: number | undefined
raised_in_lon?: number | undefined
raised_in_radius?: number | undefined
compatibleWithUserId?: string | undefined
skipId?: string | undefined
orderBy?: string | undefined
@@ -107,6 +110,9 @@ export const loadProfiles = async (props: profileQueryType) => {
lat,
lon,
radius,
raised_in_lat,
raised_in_lon,
raised_in_radius,
compatibleWithUserId,
orderBy: orderByParam = 'created_time',
lastModificationWithin,
@@ -115,6 +121,7 @@ export const loadProfiles = async (props: profileQueryType) => {
} = props
const filterLocation = lat && lon && radius
const filterRaisedInLocation = raised_in_lat && raised_in_lon && raised_in_radius
const keywords = name
? name
@@ -372,6 +379,21 @@ export const loadProfiles = async (props: profileQueryType) => {
{target_lat: lat, target_lon: lon, radius},
),
filterRaisedInLocation &&
where(
`
raised_in_lat BETWEEN $(target_lat) - ($(radius) / 69.0)
AND $(target_lat) + ($(radius) / 69.0)
AND raised_in_lon BETWEEN $(target_lon) - ($(radius) / (69.0 * COS(RADIANS($(target_lat)))))
AND $(target_lon) + ($(radius) / (69.0 * COS(RADIANS($(target_lat)))))
AND SQRT(
POWER(raised_in_lat - $(target_lat), 2)
+ POWER((raised_in_lon - $(target_lon)) * COS(RADIANS($(target_lat))), 2)
) <= $(radius) / 69.0
`,
{target_lat: raised_in_lat, target_lon: raised_in_lon, radius: raised_in_radius},
),
skipId && where(`profiles.user_id != $(skipId)`, {skipId}),
!shortBio &&

View File

@@ -3,6 +3,13 @@ import type {User} from 'common/user'
// for email template testing
// A subset of Profile with only essential fields for testing
export type PartialProfile = Pick<ProfileRow,
'id' | 'user_id' | 'created_time' | 'last_modification_time' |
'disabled' | 'looking_for_matches' | 'messaging_status' |
'comments_enabled' | 'visibility'
> & Partial<ProfileRow>
export const mockUser: User = {
createdTime: 0,
bio: 'the futa in futarchy',
@@ -26,98 +33,22 @@ export const mockUser: User = {
},
}
export const sinclairProfile: ProfileRow = {
export const sinclairProfile: PartialProfile = {
// Required fields
id: 55,
user_id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
created_time: '2023-10-27T00:41:59.851776+00:00',
last_modification_time: '2024-05-17T02:11:48.83+00:00',
city: 'San Francisco',
gender: 'trans-female',
pref_gender: ['female', 'trans-female'],
pref_age_min: 18,
pref_age_max: 21,
religion: [],
image_descriptions: {},
languages: ['english'],
pref_relation_styles: ['friendship'],
pref_romantic_styles: ['poly', 'open', 'mono'],
disabled: false,
wants_kids_strength: 3,
looking_for_matches: true,
visibility: 'public',
mbti: 'intj',
messaging_status: 'open',
comments_enabled: true,
has_kids: 0,
is_smoker: false,
drinks_per_month: 0,
diet: null,
political_beliefs: ['e/acc', 'libertarian'],
relationship_status: ['married'],
religious_belief_strength: null,
religious_beliefs: null,
political_details: '',
photo_urls: [
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FnJz22lr3Bl.jpg?alt=media&token=f1e99ba3-39cc-4637-8702-16a3a8dd49db',
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FygM0mGgP_j.HEIC?alt=media&token=573b23d9-693c-4d6e-919b-097309f370e1',
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FWPZNKxjHGV.HEIC?alt=media&token=190625e1-2cf0-49a6-824b-09b6f4002f2a',
'https://firebasestorage.googleapis.com/v0/b/polylove.firebasestorage.app/o/user-images%2FSinclair%2Flove-images%2FlVFKKoLHyV.jpg?alt=media&token=ecb3a003-3672-4382-9ba0-ca894247bb3f',
'https://firebasestorage.googleapis.com/v0/b/polylove.firebasestorage.app/o/user-images%2FSinclair%2Flove-images%2Fh659K0bmd4.jpg?alt=media&token=6561ed05-0e2d-4f31-95ee-c7c1c0b33ea6',
'https://firebasestorage.googleapis.com/v0/b/polylove.firebasestorage.app/o/user-images%2FSinclair%2Flove-images%2F5OMTo5rhB-.jpg?alt=media&token=4aba4e5a-5115-4d2e-9d57-1e6162e15708',
'https://firebasestorage.googleapis.com/v0/b/polylove.firebasestorage.app/o/user-images%2FSinclair%2Flove-images%2FwCT-Y-bgpc.jpg?alt=media&token=91994528-e436-4055-af69-421fa9e29e5c',
],
pinned_url:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FYXD19m12D7.jpg?alt=media&token=6cb095b4-dfc8-4bc9-ae67-6f12f29be0a5',
ethnicity: ['asian'],
born_in_location: null,
height_in_inches: 70,
education_level: 'bachelors',
university: 'Santa Clara University',
occupation: null,
occupation_title: 'Founding Engineer',
company: 'Manifold Markets',
website: 'sincl.ai',
twitter: 'x.com/singularitttt',
region_code: 'CA',
country: 'United States of America',
city_latitude: 37.7775,
city_longitude: -122.416389,
geodb_city_id: '126964',
referred_by_username: null,
bio_length: 1000,
bio: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://sinclaaair.notion.site/Date-Me-487ef432c1f54938bf5e7a45ef05d57b',
target: '_blank',
},
},
],
text: 'https://sinclaaair.notion.site/Date-Me-487ef432c1f54938bf5e7a45ef05d57b',
},
],
},
],
},
bio_text: 'the futa in futarchy',
bio_tsv: 'the futa in futarchy',
search_text: 'the futa in futarchy',
search_tsv: 'the futa in futarchy',
visibility: 'public',
// Optional commonly used fields for testing
city: 'San Francisco',
gender: 'trans-female',
age: 25,
big5_agreeableness: 10,
big5_openness: 10,
big5_conscientiousness: 10,
big5_extraversion: 10,
big5_neuroticism: 10,
}
export const jamesUser: User = {
@@ -141,99 +72,20 @@ export const jamesUser: User = {
},
}
export const jamesProfile: ProfileRow = {
export const jamesProfile: PartialProfile = {
// Required fields
id: 2,
user_id: '5LZ4LgYuySdL1huCWe7bti02ghx2',
created_time: '2023-10-21T21:18:26.691211+00:00',
last_modification_time: '2024-05-17T02:11:48.83+00:00',
city: 'San Francisco',
gender: 'male',
pref_gender: ['female'],
disabled: false,
pref_age_min: 22,
pref_age_max: 32,
religion: [],
image_descriptions: {},
languages: ['english'],
pref_relation_styles: ['friendship'],
pref_romantic_styles: ['poly', 'open', 'mono'],
wants_kids_strength: 4,
looking_for_matches: true,
visibility: 'public',
messaging_status: 'open',
comments_enabled: true,
has_kids: 0,
is_smoker: false,
drinks_per_month: 5,
diet: null,
political_beliefs: ['libertarian'],
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',
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2Fsii17zOItz.jpg?alt=media&token=474034b9-0d23-4005-97ad-5864abfd85fe',
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2F3ICeb-0mwB.jpg?alt=media&token=975dbdb9-5547-4553-b504-e6545eb82ec0',
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2FdtuSGk_13Q.jpg?alt=media&token=98191d86-9d10-4571-879c-d00ab9cab09e',
],
pinned_url:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2FXkLhuxZoOX.jpg?alt=media&token=7f2304dd-bace-4806-8e3c-78c35e57287c',
ethnicity: ['caucasian'],
born_in_location: 'Melbourne, FL',
height_in_inches: 70,
education_level: 'bachelors',
university: 'Carnegie Mellon',
occupation: 'Entrepreneur',
occupation_title: 'CEO',
company: 'Codebuff',
website: 'https://jamesgrugett.com/',
twitter: 'https://twitter.com/jahooma',
region_code: 'CA',
country: 'United States of America',
city_latitude: 37.7775,
city_longitude: -122.416389,
geodb_city_id: '126964',
referred_by_username: null,
bio_length: 1000,
bio: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: "Optimist that's working to improve the world!",
},
],
},
{
type: 'paragraph',
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'I like outdoor activities, hanging out with my housemates, strategy board games, libertarian and utilitarian ideas, getting boba, and riding my electric unicycle. I also enjoy working hard on bold new initiatives with huge potential for value creation!',
},
],
},
{
type: 'paragraph',
},
],
},
bio_text: 'the futa in futarchy',
bio_tsv: 'the futa in futarchy',
search_text: 'the futa in futarchy',
search_tsv: 'the futa in futarchy',
visibility: 'public',
// Optional commonly used fields for testing
city: 'San Francisco',
gender: 'male',
age: 32,
big5_agreeableness: 10,
big5_openness: 10,
big5_conscientiousness: 10,
big5_extraversion: 10,
big5_neuroticism: 10,
}

View File

@@ -47,4 +47,5 @@ BEGIN;
\i backend/supabase/migrations/20251112_add_mbti_to_profiles.sql
\i backend/supabase/migrations/20260213_add_big_5_to_profiles.sql
\i backend/supabase/migrations/20260218_add_events.sql
\i backend/supabase/migrations/20260218_add_notification_templates.sql
COMMIT;

View File

@@ -43,6 +43,13 @@ CREATE TABLE IF NOT EXISTS profiles (
pref_gender TEXT[],
pref_relation_styles TEXT[],
pref_romantic_styles TEXT[],
raised_in_city TEXT,
raised_in_country TEXT,
raised_in_geodb_city_id TEXT,
raised_in_lat NUMERIC(9, 6),
raised_in_lon NUMERIC(9, 6),
raised_in_radius INTEGER,
raised_in_region_code TEXT,
referred_by_username TEXT,
region_code TEXT,
relationship_status TEXT[],
@@ -92,9 +99,11 @@ CREATE INDEX IF NOT EXISTS idx_profiles_bio_length
-- Fastest general-purpose index
CREATE INDEX IF NOT EXISTS profiles_lat_lon_idx ON profiles (city_latitude, city_longitude);
CREATE INDEX IF NOT EXISTS profiles_lat_lon_idx ON profiles (raised_in_lat, raised_in_lon);
-- Optional additional index for large tables / clustered inserts
CREATE INDEX IF NOT EXISTS profiles_lat_lon_brin_idx ON profiles USING BRIN (city_latitude, city_longitude) WITH (pages_per_range = 32);
CREATE INDEX IF NOT EXISTS profiles_lat_lon_brin_idx ON profiles USING BRIN (raised_in_lat, raised_in_lon) WITH (pages_per_range = 32);
CREATE INDEX profiles_pref_gender_gin ON profiles USING GIN (pref_gender);
CREATE INDEX profiles_pref_relation_styles_gin ON profiles USING GIN (pref_relation_styles);
@@ -118,6 +127,7 @@ CREATE INDEX profiles_smoker_idx ON profiles (is_smoker);
CREATE INDEX profiles_education_level_idx ON profiles (education_level);
CREATE INDEX profiles_gender_idx ON profiles (gender);
CREATE INDEX profiles_geodb_city_idx ON profiles (geodb_city_id);
CREATE INDEX profiles_raised_in_geodb_city_idx ON profiles (raised_in_geodb_city_id);
CREATE INDEX profiles_recent_active_idx
ON profiles (last_modification_time DESC)

View File

@@ -548,6 +548,9 @@ export const API = (_apiTypeCheck = {
lat: z.coerce.number().optional(),
lon: z.coerce.number().optional(),
radius: z.coerce.number().optional(),
raised_in_lat: z.coerce.number().optional(),
raised_in_lon: z.coerce.number().optional(),
raised_in_radius: z.coerce.number().optional(),
compatibleWithUserId: z.string().optional(),
skipId: z.string().optional(),
locale: z.string().optional(),

View File

@@ -97,6 +97,13 @@ const optionalProfilesSchema = z.object({
political_beliefs: z.array(z.string()).optional().nullable(),
political_details: z.string().optional().nullable(),
pref_romantic_styles: z.array(z.string()).nullable(),
raised_in_city: z.string().optional().nullable(),
raised_in_country: z.string().optional().nullable(),
raised_in_geodb_city_id: z.string().optional().nullable(),
raised_in_lat: z.number().optional().nullable(),
raised_in_lon: z.number().optional().nullable(),
raised_in_radius: z.number().optional().nullable(),
raised_in_region_code: z.string().optional().nullable(),
relationship_status: z.array(z.string()).optional().nullable(),
religion: z.array(z.string()).optional().nullable(),
religious_belief_strength: z.number().optional().nullable(),

View File

@@ -15,6 +15,9 @@ export type FilterFields = {
lat: number | null
lon: number | null
radius: number | null
raised_in_lat: number | null
raised_in_lon: number | null
raised_in_radius: number | null
genders: string[]
education_levels: string[]
mbti: string[]
@@ -73,6 +76,9 @@ export const initialFilters: Partial<FilterFields> = {
lat: undefined,
lon: undefined,
radius: undefined,
raised_in_lat: undefined,
raised_in_lon: undefined,
raised_in_radius: undefined,
name: undefined,
genders: undefined,
education_levels: undefined,

View File

@@ -504,6 +504,42 @@ export type Database = {
},
]
}
notification_templates: {
Row: {
created_time: number
data: Json | null
id: string
source_slug: string | null
source_text: string
source_type: string
source_update_type: string | null
source_user_avatar_url: string | null
title: string | null
}
Insert: {
created_time: number
data?: Json | null
id: string
source_slug?: string | null
source_text: string
source_type: string
source_update_type?: string | null
source_user_avatar_url?: string | null
title?: string | null
}
Update: {
created_time?: number
data?: Json | null
id?: string
source_slug?: string | null
source_text?: string
source_type?: string
source_update_type?: string | null
source_user_avatar_url?: string | null
title?: string | null
}
Relationships: []
}
private_user_message_channel_members: {
Row: {
channel_id: number
@@ -990,6 +1026,13 @@ export type Database = {
pref_gender: string[] | null
pref_relation_styles: string[] | null
pref_romantic_styles: string[] | null
raised_in_city: string | null
raised_in_country: string | null
raised_in_geodb_city_id: string | null
raised_in_lat: number | null
raised_in_lon: number | null
raised_in_radius: number | null
raised_in_region_code: string | null
referred_by_username: string | null
region_code: string | null
relationship_status: string[] | null
@@ -1052,6 +1095,13 @@ export type Database = {
pref_gender?: string[] | null
pref_relation_styles?: string[] | null
pref_romantic_styles?: string[] | null
raised_in_city?: string | null
raised_in_country?: string | null
raised_in_geodb_city_id?: string | null
raised_in_lat?: number | null
raised_in_lon?: number | null
raised_in_radius?: number | null
raised_in_region_code?: string | null
referred_by_username?: string | null
region_code?: string | null
relationship_status?: string[] | null
@@ -1114,6 +1164,13 @@ export type Database = {
pref_gender?: string[] | null
pref_relation_styles?: string[] | null
pref_romantic_styles?: string[] | null
raised_in_city?: string | null
raised_in_country?: string | null
raised_in_geodb_city_id?: string | null
raised_in_lat?: number | null
raised_in_lon?: number | null
raised_in_radius?: number | null
raised_in_region_code?: string | null
referred_by_username?: string | null
region_code?: string | null
relationship_status?: string[] | null
@@ -1322,19 +1379,29 @@ export type Database = {
Row: {
data: Json
notification_id: string
template_id: string | null
user_id: string
}
Insert: {
data: Json
notification_id: string
template_id?: string | null
user_id: string
}
Update: {
data?: Json
notification_id?: string
template_id?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: 'user_notifications_template_id_fkey'
columns: ['template_id']
isOneToOne: false
referencedRelation: 'notification_templates'
referencedColumns: ['id']
},
{
foreignKeyName: 'user_notifications_user_id_fkey'
columns: ['user_id']

View File

@@ -6,7 +6,7 @@ import {
RACE_CHOICES,
RELATIONSHIP_CHOICES,
RELIGION_CHOICES,
} from 'common/lib/choices'
} from 'common/choices'
class UserAccountInformation {
name = faker.person.fullName()

View File

@@ -6,8 +6,6 @@
{"path": "./backend/shared"},
{"path": "./backend/shared/tsconfig.test.json"},
{"path": "./common"},
{"path": "./common/tsconfig.test.json"},
{"path": "./web"},
{"path": "./web/tsconfig.test.json"}
{"path": "./common/tsconfig.test.json"}
]
}

View File

@@ -53,6 +53,7 @@ export function DesktopFilters(props: {
setYourFilters: (checked: boolean) => void
isYourFilters: boolean
locationFilterProps: LocationFilterProps
raisedInLocationFilterProps?: LocationFilterProps
includeRelationshipFilters: boolean | undefined
choices: Record<OptionTableKey, Record<string, string>>
}) {
@@ -64,6 +65,7 @@ export function DesktopFilters(props: {
setYourFilters,
isYourFilters,
locationFilterProps,
raisedInLocationFilterProps,
includeRelationshipFilters,
choices,
} = props
@@ -152,6 +154,34 @@ export function DesktopFilters(props: {
menuWidth="w-80"
/>
{/* RAISED IN LOCATION */}
{raisedInLocationFilterProps && (
<CustomizeableDropdown
buttonContent={(open: boolean) => (
<DropdownButton
content={
<LocationFilterText
youProfile={youProfile}
location={raisedInLocationFilterProps.location}
radius={raisedInLocationFilterProps.radius ?? 100}
highlightedClass={open ? 'text-primary-500' : ''}
labelPrefix={t('filter.raised_in', 'Grew up')}
/>
}
open={open}
/>
)}
dropdownMenuContent={
<LocationFilter
youProfile={youProfile}
locationFilterProps={raisedInLocationFilterProps}
/>
}
popoverClassName="bg-canvas-50"
menuWidth="w-80"
/>
)}
{/* AGE RANGE */}
<CustomizeableDropdown
buttonContent={(open: boolean) => (

View File

@@ -21,18 +21,22 @@ export function LocationFilterText(props: {
youProfile: Profile | undefined | null
radius: number
highlightedClass?: string
labelPrefix?: string
}) {
const {location, radius, highlightedClass} = props
const {location, radius, highlightedClass, labelPrefix} = props
const {measurementSystem} = useMeasurementSystem()
const t = useT()
const locationLabel = labelPrefix
? t('filter.raised_in', labelPrefix)
: t('filter.location', 'Living')
if (!location) {
return (
<span>
<span className="">{locationLabel} </span>
<span className={clsx('text-semibold', highlightedClass)}>
{t('filter.location.any', 'Any')}
{t('filter.location.any', 'anywhere')}
</span>
<span className=""> {t('filter.location', 'location')}</span>
</span>
)
}
@@ -41,6 +45,7 @@ export function LocationFilterText(props: {
return (
<span className="font-semibold">
<span className="">{locationLabel} </span>
<span className="">
<span className={clsx(highlightedClass)}>{formattedDistance}</span>
</span>{' '}

View File

@@ -55,6 +55,7 @@ function MobileFilters(props: {
setYourFilters: (checked: boolean) => void
isYourFilters: boolean
locationFilterProps: LocationFilterProps
raisedInLocationFilterProps: LocationFilterProps
includeRelationshipFilters: boolean | undefined
choices: Record<OptionTableKey, Record<string, string>>
}) {
@@ -67,6 +68,7 @@ function MobileFilters(props: {
setYourFilters,
isYourFilters,
locationFilterProps,
raisedInLocationFilterProps,
includeRelationshipFilters,
choices,
} = props
@@ -141,7 +143,7 @@ function MobileFilters(props: {
{/* LOCATION */}
<MobileFilterSection
title={t('profile.optional.location', 'Location')}
title={t('profile.optional.location', 'Living')}
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={!!locationFilterProps.location}
@@ -157,6 +159,27 @@ function MobileFilters(props: {
<LocationFilter youProfile={youProfile} locationFilterProps={locationFilterProps} />
</MobileFilterSection>
{/* RAISED IN LOCATION */}
<MobileFilterSection
title={t('profile.optional.raised_in', 'Grew up')}
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={!!raisedInLocationFilterProps.location}
selection={
<LocationFilterText
location={raisedInLocationFilterProps.location}
radius={raisedInLocationFilterProps.radius}
labelPrefix={t('filter.raised_in', 'Grew up')}
youProfile={youProfile}
highlightedClass={
!raisedInLocationFilterProps.location ? 'text-ink-900' : 'text-primary-600'
}
/>
}
>
<LocationFilter youProfile={youProfile} locationFilterProps={raisedInLocationFilterProps} />
</MobileFilterSection>
{/* AGE RANGE */}
<MobileFilterSection
title={t('profile.optional.age', 'Age')}

View File

@@ -113,6 +113,7 @@ export const Search = forwardRef<
setYourFilters: (checked: boolean) => void
isYourFilters: boolean
locationFilterProps: LocationFilterProps
raisedInLocationFilterProps: LocationFilterProps
bookmarkedSearches: BookmarkedSearchesType[]
refreshBookmarkedSearches: () => void
profileCount: number | undefined
@@ -141,6 +142,7 @@ export const Search = forwardRef<
setOpenFiltersModal: parentSetOpenFiltersModal,
highlightFilters,
highlightSort,
raisedInLocationFilterProps,
} = props
const [internalOpenFiltersModal, setInternalOpenFiltersModal] = useState(false)
@@ -280,6 +282,7 @@ export const Search = forwardRef<
setYourFilters={setYourFilters}
isYourFilters={isYourFilters}
locationFilterProps={locationFilterProps}
raisedInLocationFilterProps={raisedInLocationFilterProps}
includeRelationshipFilters={youSeekingRelationship}
choices={choices}
/>
@@ -297,6 +300,7 @@ export const Search = forwardRef<
setYourFilters={setYourFilters}
isYourFilters={isYourFilters}
locationFilterProps={locationFilterProps}
raisedInLocationFilterProps={raisedInLocationFilterProps}
includeRelationshipFilters={youSeekingRelationship}
choices={choices}
/>

View File

@@ -30,6 +30,7 @@ export const useFilters = (you: Profile | undefined) => {
const clearFilters = () => {
setFilters(isLooking ? initialFilters : {...initialFilters, orderBy: 'created_time'})
setLocation(undefined)
setRaisedInLocation(undefined)
}
const [radius, setRadius] = usePersistentLocalState<number>(100, 'search-radius')
@@ -41,6 +42,19 @@ export const useFilters = (you: Profile | undefined) => {
'nearby-origin-location',
)
const [raisedInRadius, setRaisedInRadius] = usePersistentLocalState<number>(
100,
'raised-in-radius',
)
const debouncedSetRaisedInRadius = useCallback(debounce(setRaisedInRadius, 200), [
setRaisedInRadius,
])
const [raisedInLocation, setRaisedInLocation] = usePersistentLocalState<
OriginLocation | undefined | null
>(undefined, 'raised-in-location')
// const nearbyCities = useNearbyCities(location?.id, radius)
//
// useEffectCheckEquality(() => {
@@ -55,6 +69,22 @@ export const useFilters = (you: Profile | undefined) => {
}
}, [location?.id, radius])
useEffect(() => {
if (raisedInLocation?.lat && raisedInLocation?.lon) {
updateFilter({
raised_in_lat: raisedInLocation.lat,
raised_in_lon: raisedInLocation.lon,
raised_in_radius: raisedInRadius,
})
} else {
updateFilter({
raised_in_lat: undefined,
raised_in_lon: undefined,
raised_in_radius: undefined,
})
}
}, [raisedInLocation?.id, raisedInRadius])
const locationFilterProps = {
location,
setLocation,
@@ -62,6 +92,13 @@ export const useFilters = (you: Profile | undefined) => {
setRadius: debouncedSetRadius,
}
const raisedInLocationFilterProps = {
location: raisedInLocation,
setLocation: setRaisedInLocation,
radius: raisedInRadius,
setRadius: debouncedSetRaisedInRadius,
}
const yourFilters: Partial<FilterFields> = {
pref_gender: you?.gender?.length ? [you.gender] : undefined,
genders: you?.pref_gender?.length ? you.pref_gender : undefined,
@@ -146,6 +183,7 @@ export const useFilters = (you: Profile | undefined) => {
setYourFilters,
isYourFilters,
locationFilterProps,
raisedInLocationFilterProps,
}
}

12
web/components/icons.tsx Normal file
View File

@@ -0,0 +1,12 @@
import {ReactNode} from 'react'
import {Row} from 'web/components/layout/row'
export function IconWithInfo(props: {text: string; icon: ReactNode}) {
const {text, icon} = props
return (
<Row className="items-center gap-0.5">
<div className="text-ink-500">{icon}</div>
{text}
</Row>
)
}

View File

@@ -15,7 +15,7 @@ import {
ROMANTIC_CHOICES,
} from 'common/choices'
import {MultipleChoiceOptions} from 'common/profiles/multiple-choice'
import {getProfileRow, ProfileWithoutUser} from 'common/profiles/profile'
import {getProfileRow, Profile, ProfileWithoutUser} from 'common/profiles/profile'
import {PLATFORM_LABELS, type Site, SITE_ORDER} from 'common/socials'
import {User} from 'common/user'
import {removeUndefinedProps} from 'common/util/object'
@@ -243,6 +243,40 @@ export const OptionalProfileUserForm = (props: {
}
}
function profileToRaisedInCity(profile: Profile): City | undefined {
if (profile.raised_in_geodb_city_id && profile.raised_in_lat && profile.raised_in_lon) {
return {
geodb_city_id: profile.raised_in_geodb_city_id,
city: profile.raised_in_city ?? null,
region_code: profile.raised_in_region_code ?? '',
country: profile.raised_in_country ?? '',
country_code: '',
latitude: profile.raised_in_lat,
longitude: profile.raised_in_lon,
}
}
return undefined
}
function setProfileRaisedInCity(inputCity: City | undefined) {
if (!inputCity) {
setProfile('raised_in_geodb_city_id', null)
setProfile('raised_in_city', null)
setProfile('raised_in_region_code', null)
setProfile('raised_in_country', null)
setProfile('raised_in_lat', null)
setProfile('raised_in_lon', null)
} else {
const {geodb_city_id, city, region_code, country, latitude, longitude} = inputCity
setProfile('raised_in_geodb_city_id', geodb_city_id)
setProfile('raised_in_city', city)
setProfile('raised_in_region_code', region_code)
setProfile('raised_in_country', country)
setProfile('raised_in_lat', latitude)
setProfile('raised_in_lon', longitude)
}
}
return (
<>
{/*<Row className={'justify-end'}>*/}
@@ -431,6 +465,41 @@ export const OptionalProfileUserForm = (props: {
/>
</Col>
<Col className={clsx(colClassName)}>
<label className={clsx(labelClassName)}>
{t('profile.optional.raised_in', 'Place you grew up')}
</label>
<label className={clsx('guidance')}>
{t(
'profile.optional.raised_in_hint',
'Especially useful if you grew up in a different country than where you live now—and if it reflects your cultural references, values, and life experiences.',
)}
</label>
{profile.raised_in_geodb_city_id ? (
<Row className="border-primary-500 w-full justify-between rounded border px-4 py-2">
<CityRow
city={profileToRaisedInCity(profile as Profile)!}
onSelect={() => {}}
className="pointer-events-none"
/>
<button
className="text-ink-700 hover:text-primary-700 text-sm underline"
onClick={() => {
setProfileRaisedInCity(undefined)
}}
>
{t('common.change', 'Change')}
</button>
</Row>
) : (
<CitySearchBox
onCitySelected={(city: City | undefined) => {
setProfileRaisedInCity(city)
}}
/>
)}
</Col>
<Category title={t('profile.optional.category.interested_in', "Who I'm looking for")} />
<Col className={clsx(colClassName)}>

View File

@@ -14,6 +14,7 @@ import {convertGenderPlural, Gender} from 'common/gender'
import {formatHeight, MeasurementSystem} from 'common/measurement-utils'
import {Profile} from 'common/profiles/profile'
import {UserActivity} from 'common/user'
import {Home} from 'lucide-react'
import React, {ReactNode} from 'react'
import {BiSolidDrink} from 'react-icons/bi'
import {BsPersonHeart, BsPersonVcard} from 'react-icons/bs'
@@ -29,6 +30,7 @@ import {RiScales3Line} from 'react-icons/ri'
import {TbBulb, TbCheck, TbMoodSad, TbUsers} from 'react-icons/tb'
import {Col} from 'web/components/layout/col'
import {Row} from 'web/components/layout/row'
import {getLocationText} from 'web/components/profile/profile-location'
import {UserHandles} from 'web/components/user/user-handles'
import {useChoices} from 'web/hooks/use-choices'
import {useLocale, useT} from 'web/lib/locale'
@@ -151,6 +153,7 @@ export default function ProfileAbout(props: {
?.map((r: any) => t(`profile.race.${r}`, convertRace(r)))}
testId="profile-about-ethnicity"
/>
<RaisedIn profile={profile} />
<Smoker profile={profile} />
<Drinks profile={profile} />
<AboutRow
@@ -505,6 +508,20 @@ function HasKids(props: {profile: Profile}) {
return <AboutRow icon={icon} text={hasKidsText} testId={'profile-about-has-kids'} />
}
function RaisedIn(props: {profile: Profile}) {
const t = useT()
const locationText = getLocationText(props.profile, 'raised_in_')
if (!locationText) {
return null
}
return (
<AboutRow
icon={<Home className="h-5 w-5" />}
text={t('profile.about.raised_in', `Raised in ${locationText}`, {location: locationText})}
/>
)
}
export const formatProfileValue = (
key: string,
value: any,

View File

@@ -92,7 +92,7 @@ export default function ProfileHeader(props: {
) : (
<span className="font-semibold">{user.name}</span>
)}
{profile.age ? `, ${profile.age}` : ''}
{profile.age && `, ${t('profile.header.age', '{age}', {age: profile.age})}`}
</span>
</Row>
<ProfilePrimaryInfo profile={profile} />

View File

@@ -0,0 +1,29 @@
import {Profile} from 'common/profiles/profile'
import {IoLocationOutline} from 'react-icons/io5'
import {IconWithInfo} from 'web/components/icons'
export function getLocationText(profile: Profile, prefix?: string) {
const city = profile[`${prefix}city` as keyof Profile]
const country = profile[`${prefix}country` as keyof Profile]
const regionCode = profile[`${prefix}region_code` as keyof Profile]
const stateOrCountry = country === 'United States of America' ? regionCode : country
if (!city) {
return null
}
return `${city}${stateOrCountry && ', '}${stateOrCountry}`
}
export function ProfileLocation(props: {profile: Profile; prefix?: string}) {
const {profile, prefix = ''} = props
const text = getLocationText(profile, prefix)
if (!text) {
return null
}
return <IconWithInfo text={text} icon={<IoLocationOutline className="h-4 w-4" />} />
}

View File

@@ -1,30 +1,23 @@
import {convertGender, Gender} from 'common/gender'
import {Profile} from 'common/profiles/profile'
import {capitalize} from 'lodash'
import {ReactNode} from 'react'
import {IoLocationOutline} from 'react-icons/io5'
import {MdHeight} from 'react-icons/md'
import {IconWithInfo} from 'web/components/icons'
import {Row} from 'web/components/layout/row'
import {useMeasurementSystem} from 'web/hooks/use-measurement-system'
import {useT} from 'web/lib/locale'
import GenderIcon from '../gender-icon'
import {formatProfileValue} from '../profile-about'
import {ProfileLocation} from './profile-location'
export default function ProfilePrimaryInfo(props: {profile: Profile}) {
const {profile} = props
const t = useT()
const {measurementSystem} = useMeasurementSystem()
const stateOrCountry =
profile.country === 'United States of America' ? profile.region_code : profile.country
return (
<Row className="text-ink-700 gap-4 text-sm" data-testid="profile-gender-location-height-inches">
{profile.city && (
<IconWithInfo
text={`${profile.city ?? ''}, ${stateOrCountry ?? ''}`}
icon={<IoLocationOutline className="h-4 w-4" />}
/>
)}
<ProfileLocation profile={profile} />
{profile.gender && (
<IconWithInfo
text={capitalize(
@@ -42,13 +35,3 @@ export default function ProfilePrimaryInfo(props: {profile: Profile}) {
</Row>
)
}
function IconWithInfo(props: {text: string; icon: ReactNode}) {
const {text, icon} = props
return (
<Row className="items-center gap-0.5">
<div className="text-ink-500">{icon}</div>
{text}
</Row>
)
}

View File

@@ -28,8 +28,15 @@ export function ProfilesHome() {
const user = useUser()
const you = useProfile()
const {filters, updateFilter, clearFilters, setYourFilters, isYourFilters, locationFilterProps} =
useFilters(you ?? undefined)
const {
filters,
updateFilter,
clearFilters,
setYourFilters,
isYourFilters,
locationFilterProps,
raisedInLocationFilterProps,
} = useFilters(you ?? undefined)
const [profiles, setProfiles] = usePersistentInMemoryState<Profile[] | undefined>(
undefined,
@@ -260,6 +267,7 @@ export function ProfilesHome() {
setYourFilters={setYourFilters}
isYourFilters={isYourFilters}
locationFilterProps={locationFilterProps}
raisedInLocationFilterProps={raisedInLocationFilterProps}
bookmarkedSearches={bookmarkedSearches}
refreshBookmarkedSearches={refreshBookmarkedSearches}
profileCount={profileCount}

View File

@@ -221,12 +221,6 @@
"filter.gender.gender": "Geschlecht",
"filter.gender.genders": "Geschlechter",
"filter.gender.they_seek": "Geschlecht, das sie suchen",
"filter.location": "Standort",
"filter.near": "in der Nähe von",
"filter.location.any": "Jeder",
"filter.location.distance_miles": "Entfernung (Meilen)",
"filter.location.search_city": "Stadt suchen...",
"filter.location.set_any_city": "Jede Stadt",
"filter.mine_toggle": "Meine Filter",
"filter.multiple": "Mehrere",
"filter.relationship.any_connection": "Jede Beziehung",
@@ -724,6 +718,16 @@
"profile.optional.username_or_url": "Benutzername oder URL",
"profile.optional.want_kids": "Ich möchte Kinder haben",
"profile.optional.work": "Arbeit",
"profile.optional.raised_in": "Ort, an dem ich aufgewachsen bin",
"profile.optional.raised_in_hint": "Besonders nützlich, wenn Sie in einem anderen Land aufgewachsen sind als dem, in dem Sie jetzt leben und wenn dies Ihre kulturellen Referenzen, Werte und Lebenserfahrungen widerspiegelt.",
"profile.about.raised_in": "Aufgewachsen in {location}",
"profile.header.age": "{age}",
"filter.raised_in": "Aufgewachsen",
"filter.location": "Wohnhaft",
"filter.location.any": "überall",
"filter.near": "in der Nähe von",
"filter.location.search_city": "Stadt suchen...",
"filter.location.set_any_city": "Auf jede Stadt einstellen",
"profile.big5_openness": "Offenheit",
"profile.big5_conscientiousness": "Gewissenhaftigkeit",
"profile.big5_extraversion": "Extraversion",

View File

@@ -218,12 +218,11 @@
"filter.gender.gender": "genre",
"filter.gender.genders": "genres",
"filter.gender.they_seek": "Genre qu'ils recherchent",
"filter.location": "localisation",
"filter.near": "près de",
"filter.location.any": "Toute",
"filter.location.distance_miles": "Distance (miles)",
"filter.location.search_city": "Rechercher une ville...",
"filter.location.set_any_city": "N'importe quelle ville",
"filter.location": "Vit",
"filter.location.any": "n'importe où",
"filter.near": "près de",
"filter.location.search_city": "Rechercher une ville...",
"filter.mine_toggle": "Mes filtres",
"filter.multiple": "Multiple",
"filter.relationship.any_connection": "Toute relation",
@@ -721,6 +720,11 @@
"profile.optional.username_or_url": "Nom d'utilisateur ou URL",
"profile.optional.want_kids": "Je souhaite avoir des enfants",
"profile.optional.work": "Domaine de travail",
"profile.optional.raised_in": "Lieu où j'ai grandi",
"profile.optional.raised_in_hint": "Particulièrement utile si vous avez grandi dans un pays différent de celui où vous vivez maintenant et si cela reflète vos références culturelles, vos valeurs et vos expériences de vie.",
"profile.about.raised_in": "A grandi à {location}",
"profile.header.age": "{age} ans",
"filter.raised_in": "A grandi",
"profile.big5_openness": "Ouverture",
"profile.big5_conscientiousness": "Conscienciosité",
"profile.big5_extraversion": "Extraversion",