diff --git a/backend/api/package.json b/backend/api/package.json index 6a964e01..febf578b 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -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", diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index af990e27..52253bd6 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -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 && diff --git a/backend/email/emails/functions/mock.ts b/backend/email/emails/functions/mock.ts index a3154ac2..c8deaa08 100644 --- a/backend/email/emails/functions/mock.ts +++ b/backend/email/emails/functions/mock.ts @@ -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 & Partial + 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, } diff --git a/backend/supabase/migration.sql b/backend/supabase/migration.sql index 279557b1..12c61bf7 100644 --- a/backend/supabase/migration.sql +++ b/backend/supabase/migration.sql @@ -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; diff --git a/backend/supabase/profiles.sql b/backend/supabase/profiles.sql index 2e7cc195..39bc6fc5 100644 --- a/backend/supabase/profiles.sql +++ b/backend/supabase/profiles.sql @@ -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) diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 3152ab5b..c6381fc3 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -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(), diff --git a/common/src/api/zod-types.ts b/common/src/api/zod-types.ts index 639aa459..9d8b3973 100644 --- a/common/src/api/zod-types.ts +++ b/common/src/api/zod-types.ts @@ -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(), diff --git a/common/src/filters.ts b/common/src/filters.ts index c1e80de3..ac91a2ec 100644 --- a/common/src/filters.ts +++ b/common/src/filters.ts @@ -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 = { 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, diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index c705d86f..4a8a1987 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -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'] diff --git a/tests/e2e/backend/utils/userInformation.ts b/tests/e2e/backend/utils/userInformation.ts index af8fde76..f1dbe699 100644 --- a/tests/e2e/backend/utils/userInformation.ts +++ b/tests/e2e/backend/utils/userInformation.ts @@ -6,7 +6,7 @@ import { RACE_CHOICES, RELATIONSHIP_CHOICES, RELIGION_CHOICES, -} from 'common/lib/choices' +} from 'common/choices' class UserAccountInformation { name = faker.person.fullName() diff --git a/tsconfig.json b/tsconfig.json index 7a3a7813..47184498 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"} ] } diff --git a/web/components/filters/desktop-filters.tsx b/web/components/filters/desktop-filters.tsx index 7e688b29..78dd493f 100644 --- a/web/components/filters/desktop-filters.tsx +++ b/web/components/filters/desktop-filters.tsx @@ -53,6 +53,7 @@ export function DesktopFilters(props: { setYourFilters: (checked: boolean) => void isYourFilters: boolean locationFilterProps: LocationFilterProps + raisedInLocationFilterProps?: LocationFilterProps includeRelationshipFilters: boolean | undefined choices: Record> }) { @@ -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 && ( + ( + + } + open={open} + /> + )} + dropdownMenuContent={ + + } + popoverClassName="bg-canvas-50" + menuWidth="w-80" + /> + )} + {/* AGE RANGE */} ( diff --git a/web/components/filters/location-filter.tsx b/web/components/filters/location-filter.tsx index 3309e2ad..a3ee4e39 100644 --- a/web/components/filters/location-filter.tsx +++ b/web/components/filters/location-filter.tsx @@ -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 ( + {locationLabel} - {t('filter.location.any', 'Any')} + {t('filter.location.any', 'anywhere')} - {t('filter.location', 'location')} ) } @@ -41,6 +45,7 @@ export function LocationFilterText(props: { return ( + {locationLabel} {formattedDistance} {' '} diff --git a/web/components/filters/mobile-filters.tsx b/web/components/filters/mobile-filters.tsx index 301db973..2c15c9a0 100644 --- a/web/components/filters/mobile-filters.tsx +++ b/web/components/filters/mobile-filters.tsx @@ -55,6 +55,7 @@ function MobileFilters(props: { setYourFilters: (checked: boolean) => void isYourFilters: boolean locationFilterProps: LocationFilterProps + raisedInLocationFilterProps: LocationFilterProps includeRelationshipFilters: boolean | undefined choices: Record> }) { @@ -67,6 +68,7 @@ function MobileFilters(props: { setYourFilters, isYourFilters, locationFilterProps, + raisedInLocationFilterProps, includeRelationshipFilters, choices, } = props @@ -141,7 +143,7 @@ function MobileFilters(props: { {/* LOCATION */} + {/* RAISED IN LOCATION */} + + } + > + + + {/* AGE RANGE */} 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} /> diff --git a/web/components/filters/use-filters.ts b/web/components/filters/use-filters.ts index f4e6846a..72938fa9 100644 --- a/web/components/filters/use-filters.ts +++ b/web/components/filters/use-filters.ts @@ -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(100, 'search-radius') @@ -41,6 +42,19 @@ export const useFilters = (you: Profile | undefined) => { 'nearby-origin-location', ) + const [raisedInRadius, setRaisedInRadius] = usePersistentLocalState( + 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 = { 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, } } diff --git a/web/components/icons.tsx b/web/components/icons.tsx new file mode 100644 index 00000000..55dad6df --- /dev/null +++ b/web/components/icons.tsx @@ -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 ( + +
{icon}
+ {text} +
+ ) +} diff --git a/web/components/optional-profile-form.tsx b/web/components/optional-profile-form.tsx index c59da460..36ece2a0 100644 --- a/web/components/optional-profile-form.tsx +++ b/web/components/optional-profile-form.tsx @@ -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 ( <> {/**/} @@ -431,6 +465,41 @@ export const OptionalProfileUserForm = (props: { /> + + + + {profile.raised_in_geodb_city_id ? ( + + {}} + className="pointer-events-none" + /> + + + ) : ( + { + setProfileRaisedInCity(city) + }} + /> + )} + + diff --git a/web/components/profile-about.tsx b/web/components/profile-about.tsx index c0950085..e7920798 100644 --- a/web/components/profile-about.tsx +++ b/web/components/profile-about.tsx @@ -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" /> + } +function RaisedIn(props: {profile: Profile}) { + const t = useT() + const locationText = getLocationText(props.profile, 'raised_in_') + if (!locationText) { + return null + } + return ( + } + text={t('profile.about.raised_in', `Raised in ${locationText}`, {location: locationText})} + /> + ) +} + export const formatProfileValue = ( key: string, value: any, diff --git a/web/components/profile/profile-header.tsx b/web/components/profile/profile-header.tsx index 094b2653..03761b89 100644 --- a/web/components/profile/profile-header.tsx +++ b/web/components/profile/profile-header.tsx @@ -92,7 +92,7 @@ export default function ProfileHeader(props: { ) : ( {user.name} )} - {profile.age ? `, ${profile.age}` : ''} + {profile.age && `, ${t('profile.header.age', '{age}', {age: profile.age})}`}
diff --git a/web/components/profile/profile-location.tsx b/web/components/profile/profile-location.tsx new file mode 100644 index 00000000..2a73ffb9 --- /dev/null +++ b/web/components/profile/profile-location.tsx @@ -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 } /> +} diff --git a/web/components/profile/profile-primary-info.tsx b/web/components/profile/profile-primary-info.tsx index 500ba71b..5bee346b 100644 --- a/web/components/profile/profile-primary-info.tsx +++ b/web/components/profile/profile-primary-info.tsx @@ -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 ( - {profile.city && ( - } - /> - )} + {profile.gender && ( ) } - -function IconWithInfo(props: {text: string; icon: ReactNode}) { - const {text, icon} = props - return ( - -
{icon}
- {text} -
- ) -} diff --git a/web/components/profiles/profiles-home.tsx b/web/components/profiles/profiles-home.tsx index bc63bd53..75303c89 100644 --- a/web/components/profiles/profiles-home.tsx +++ b/web/components/profiles/profiles-home.tsx @@ -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( undefined, @@ -260,6 +267,7 @@ export function ProfilesHome() { setYourFilters={setYourFilters} isYourFilters={isYourFilters} locationFilterProps={locationFilterProps} + raisedInLocationFilterProps={raisedInLocationFilterProps} bookmarkedSearches={bookmarkedSearches} refreshBookmarkedSearches={refreshBookmarkedSearches} profileCount={profileCount} diff --git a/web/messages/de.json b/web/messages/de.json index fabf82fa..e4450b3a 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -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", diff --git a/web/messages/fr.json b/web/messages/fr.json index aa315acf..7c199c0b 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -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",