mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-24 19:06:37 -05:00
Add bookmarked search emails and factor out utils from web to common
This commit is contained in:
@@ -5,7 +5,6 @@ import {from, join, limit, orderBy, renderSql, select, where,} from 'shared/supa
|
||||
import {getCompatibleLovers} from 'api/compatible-lovers'
|
||||
import {intersection} from 'lodash'
|
||||
import {MAX_INT, MIN_INT} from "common/constants";
|
||||
import {Lover} from "common/love/lover";
|
||||
|
||||
export type profileQueryType = {
|
||||
limit?: number | undefined,
|
||||
@@ -22,6 +21,7 @@ export type profileQueryType = {
|
||||
is_smoker?: boolean | undefined,
|
||||
geodbCityIds?: String[] | undefined,
|
||||
compatibleWithUserId?: string | undefined,
|
||||
skipId?: string | undefined,
|
||||
orderBy?: string | undefined,
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
geodbCityIds,
|
||||
compatibleWithUserId,
|
||||
orderBy: orderByParam,
|
||||
skipId,
|
||||
} = props
|
||||
|
||||
const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : []
|
||||
@@ -50,7 +51,10 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
|
||||
// compatibility. TODO: do this in sql
|
||||
if (orderByParam === 'compatibility_score') {
|
||||
if (!compatibleWithUserId) return {status: 'fail', lovers: []}
|
||||
if (!compatibleWithUserId) {
|
||||
console.error('Incompatible with user ID')
|
||||
throw Error('Incompatible with user ID')
|
||||
}
|
||||
|
||||
const {compatibleLovers} = await getCompatibleLovers(compatibleWithUserId)
|
||||
const lovers = compatibleLovers.filter(
|
||||
@@ -72,6 +76,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
(has_kids == 0 && !l.has_kids) ||
|
||||
(l.has_kids && l.has_kids > 0)) &&
|
||||
(!is_smoker || l.is_smoker === is_smoker) &&
|
||||
(l.id.toString() != skipId) &&
|
||||
(!geodbCityIds ||
|
||||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id)))
|
||||
)
|
||||
@@ -135,6 +140,8 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
geodbCityIds?.length &&
|
||||
where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}),
|
||||
|
||||
skipId && where(`user_id != $(skipId)`, {skipId}),
|
||||
|
||||
orderBy(`${orderByParam} desc`),
|
||||
after &&
|
||||
where(
|
||||
@@ -151,6 +158,10 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
}
|
||||
|
||||
export const getProfiles: APIHandler<'get-profiles'> = async (props, _auth) => {
|
||||
const lovers = await loadProfiles(props)
|
||||
return {status: 'success', lovers: lovers as Lover[]}
|
||||
try {
|
||||
const lovers = await loadProfiles(props)
|
||||
return {status: 'success', lovers: lovers}
|
||||
} catch {
|
||||
return {status: 'fail', lovers: []}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import {APIHandler} from './helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from "shared/supabase/init";
|
||||
import {convertRow} from "shared/love/supabase";
|
||||
import {from, join, renderSql, select, where} from "shared/supabase/sql-builder";
|
||||
import {from, renderSql, select} from "shared/supabase/sql-builder";
|
||||
import {loadProfiles, profileQueryType} from "api/get-profiles";
|
||||
import {Row} from "common/supabase/utils";
|
||||
import {sendSearchAlertsEmail} from "email/functions/helpers";
|
||||
import {MatchesByUserType} from "common/love/bookmarked_searches";
|
||||
import {keyBy} from "lodash";
|
||||
|
||||
export function convertSearchRow(row: any): any {
|
||||
return row
|
||||
}
|
||||
|
||||
|
||||
export const notifyBookmarkedSearch = async (matches: MatchesByUserType) => {
|
||||
for (const [_, value] of Object.entries(matches)) {
|
||||
await sendSearchAlertsEmail(value.user, value.privateUser, value.matches)
|
||||
}
|
||||
}
|
||||
|
||||
export const sendSearchNotifications: APIHandler<'send-search-notifications'> = async (_, auth) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
@@ -14,20 +25,58 @@ export const sendSearchNotifications: APIHandler<'send-search-notifications'> =
|
||||
select('bookmarked_searches.*'),
|
||||
from('bookmarked_searches'),
|
||||
)
|
||||
const searches = pg.map(search_query, [], convertSearchRow)
|
||||
const searches = await pg.map(search_query, [], convertSearchRow) as Row<'bookmarked_searches'>[]
|
||||
console.log(`Running ${searches.length} bookmarked searches`)
|
||||
|
||||
const query = renderSql(
|
||||
select('lovers.*, name, username, users.data as user'),
|
||||
from('lovers'),
|
||||
join('users on users.id = lovers.user_id'),
|
||||
where('looking_for_matches = true'),
|
||||
where(
|
||||
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
|
||||
const _users = await pg.map(
|
||||
renderSql(
|
||||
select('users.*'),
|
||||
from('users'),
|
||||
),
|
||||
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
|
||||
)
|
||||
[],
|
||||
convertSearchRow
|
||||
) as Row<'users'>[]
|
||||
const users = keyBy(_users, 'id')
|
||||
console.log('users', users)
|
||||
|
||||
const profiles = await pg.map(query, [], convertRow)
|
||||
const _privateUsers = await pg.map(
|
||||
renderSql(
|
||||
select('private_users.*'),
|
||||
from('private_users'),
|
||||
),
|
||||
[],
|
||||
convertSearchRow
|
||||
) as Row<'private_users'>[]
|
||||
const privateUsers = keyBy(_privateUsers, 'id')
|
||||
console.log('privateUsers', privateUsers)
|
||||
|
||||
return {status: 'success', lovers: profiles}
|
||||
const matches: MatchesByUserType = {}
|
||||
|
||||
for (const row of searches) {
|
||||
if (typeof row.search_filters !== 'object') continue;
|
||||
const props = {...row.search_filters, skipId: row.creator_id}
|
||||
const profiles = await loadProfiles(props as profileQueryType)
|
||||
console.log(profiles.map((item: any) => item.name))
|
||||
if (!profiles.length) continue
|
||||
if (!(row.creator_id in matches)) {
|
||||
if (!privateUsers[row.creator_id]) continue
|
||||
matches[row.creator_id] = {
|
||||
user: users[row.creator_id],
|
||||
privateUser: privateUsers[row.creator_id]['data'],
|
||||
matches: [],
|
||||
}
|
||||
}
|
||||
matches[row.creator_id].matches.push({
|
||||
id: row.creator_id,
|
||||
description: {filters: row.search_filters, location: row.location},
|
||||
matches: [profiles.map((item: any) => ({
|
||||
name: item.name,
|
||||
username: item.username,
|
||||
}))],
|
||||
})
|
||||
}
|
||||
console.log(JSON.stringify(matches, null, 2))
|
||||
await notifyBookmarkedSearch(matches)
|
||||
|
||||
return {status: 'success'}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { PrivateUser, User } from 'common/user'
|
||||
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
||||
import { sendEmail } from './send-email'
|
||||
import { NewMatchEmail } from '../new-match'
|
||||
import { NewMessageEmail } from '../new-message'
|
||||
import { NewEndorsementEmail } from '../new-endorsement'
|
||||
import { Test } from '../test'
|
||||
import { getLover } from 'shared/love/supabase'
|
||||
import {PrivateUser, User} from 'common/user'
|
||||
import {getNotificationDestinationsForUser} from 'common/user-notification-preferences'
|
||||
import {sendEmail} from './send-email'
|
||||
import {NewMatchEmail} from '../new-match'
|
||||
import {NewMessageEmail} from '../new-message'
|
||||
import {NewEndorsementEmail} from '../new-endorsement'
|
||||
import {Test} from '../test'
|
||||
import {getLover} from 'shared/love/supabase'
|
||||
import {renderToStaticMarkup} from "react-dom/server";
|
||||
import {MatchesType} from "common/love/bookmarked_searches";
|
||||
import NewSearchAlertsEmail from "email/new-search_alerts";
|
||||
|
||||
const from = 'Compass <no-reply@compassmeet.com>'
|
||||
|
||||
@@ -14,7 +16,7 @@ export const sendNewMatchEmail = async (
|
||||
privateUser: PrivateUser,
|
||||
matchedWithUser: User
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_match'
|
||||
)
|
||||
@@ -44,7 +46,7 @@ export const sendNewMessageEmail = async (
|
||||
toUser: User,
|
||||
channelId: number
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_message'
|
||||
)
|
||||
@@ -57,22 +59,6 @@ export const sendNewMessageEmail = async (
|
||||
return
|
||||
}
|
||||
|
||||
console.log({
|
||||
from,
|
||||
subject: `${fromUser.name} sent you a message!`,
|
||||
to: privateUser.email,
|
||||
html: renderToStaticMarkup(
|
||||
<NewMessageEmail
|
||||
fromUser={fromUser}
|
||||
fromUserLover={lover}
|
||||
toUser={toUser}
|
||||
channelId={channelId}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
email={privateUser.email}
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
return await sendEmail({
|
||||
from,
|
||||
subject: `${fromUser.name} sent you a message!`,
|
||||
@@ -90,13 +76,40 @@ export const sendNewMessageEmail = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const sendSearchAlertsEmail = async (
|
||||
toUser: User,
|
||||
privateUser: PrivateUser,
|
||||
matches: MatchesType[],
|
||||
) => {
|
||||
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_search_alerts'
|
||||
)
|
||||
const email = privateUser.email;
|
||||
if (!email || !sendToEmail) return
|
||||
|
||||
return await sendEmail({
|
||||
from,
|
||||
subject: `Some people recently matched your bookmarked searches!`,
|
||||
to: email,
|
||||
html: renderToStaticMarkup(
|
||||
<NewSearchAlertsEmail
|
||||
toUser={toUser}
|
||||
matches={matches}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
email={email}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const sendNewEndorsementEmail = async (
|
||||
privateUser: PrivateUser,
|
||||
fromUser: User,
|
||||
onUser: User,
|
||||
text: string
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_endorsement'
|
||||
)
|
||||
@@ -123,6 +136,6 @@ export const sendTestEmail = async (toEmail: string) => {
|
||||
from,
|
||||
subject: 'Test email from Compass',
|
||||
to: toEmail,
|
||||
html: renderToStaticMarkup(<Test name="Test User" />),
|
||||
html: renderToStaticMarkup(<Test name="Test User"/>),
|
||||
})
|
||||
}
|
||||
|
||||
129
backend/email/emails/new-search_alerts.tsx
Normal file
129
backend/email/emails/new-search_alerts.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import {type User} from 'common/user'
|
||||
import {type LoverRow} from 'common/love/lover'
|
||||
import {
|
||||
jamesLover,
|
||||
jamesUser,
|
||||
sinclairLover,
|
||||
sinclairUser,
|
||||
} from './functions/mock'
|
||||
import {DOMAIN} from 'common/envs/constants'
|
||||
import {getLoveOgImageUrl} from 'common/love/og-image'
|
||||
import {button, container, content, Footer, imageContainer, main, paragraph, profileImage} from "email/utils";
|
||||
import {sendSearchAlertsEmail} from "email/functions/helpers";
|
||||
import {MatchesType} from "common/love/bookmarked_searches";
|
||||
import {formatFilters, locationType} from "common/searches"
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
interface NewMessageEmailProps {
|
||||
toUser: User
|
||||
matches: MatchesType[]
|
||||
unsubscribeUrl: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export const NewSearchAlertsEmail = ({
|
||||
toUser,
|
||||
unsubscribeUrl,
|
||||
matches,
|
||||
email,
|
||||
}: NewMessageEmailProps) => {
|
||||
const name = toUser.name.split(' ')[0]
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head/>
|
||||
<Preview>Some people recently matched your bookmarked searches!</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Section style={content}>
|
||||
<Text style={paragraph}>Hi {name},</Text>
|
||||
<Text style={paragraph}>The following people matched your bookmarked searches in the past 24h:</Text>
|
||||
<Text
|
||||
className={
|
||||
'border-ink-300bg-canvas-0 inline-flex flex-col gap-2 rounded-md border p-1 text-sm shadow-sm'
|
||||
}
|
||||
>
|
||||
<ol className="list-decimal list-inside space-y-2">
|
||||
{(matches || []).map((match) => (
|
||||
<li key={match.id}
|
||||
className="items-center justify-between gap-2 list-item marker:text-ink-500 marker:font-bold">
|
||||
{formatFilters(match.description.filters as Partial<FilterFields>, match.description.location as locationType)}
|
||||
<span>{match.matches.map(p => p.toString()).join(', ')}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
</Text>
|
||||
</Section>
|
||||
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
const matchSamples = [
|
||||
{
|
||||
"id": "ID search 1",
|
||||
"description": {
|
||||
"filters": {
|
||||
"orderBy": "created_time"
|
||||
},
|
||||
"location": null
|
||||
},
|
||||
"matches": [
|
||||
[
|
||||
{
|
||||
"name": "James Bond",
|
||||
"username": "jamesbond"
|
||||
},
|
||||
{
|
||||
"name": "Lily",
|
||||
"username": "lilyrose"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ID search 2",
|
||||
"description": {
|
||||
"filters": {
|
||||
"genders": [
|
||||
"female"
|
||||
],
|
||||
"orderBy": "created_time"
|
||||
},
|
||||
"location": null
|
||||
},
|
||||
"matches": [
|
||||
[
|
||||
{
|
||||
"name": "Lily",
|
||||
"username": "lilyrose"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
NewSearchAlertsEmail.PreviewProps = {
|
||||
toUser: sinclairUser,
|
||||
email: 'someone@gmail.com',
|
||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||
matches: matchSamples,
|
||||
} as NewMessageEmailProps
|
||||
|
||||
|
||||
export default NewSearchAlertsEmail
|
||||
@@ -57,7 +57,6 @@ export const API = (_apiTypeCheck = {
|
||||
props: z.object({}),
|
||||
returns: {} as {
|
||||
status: 'success' | 'fail'
|
||||
lovers: Lover[]
|
||||
},
|
||||
},
|
||||
'mark-all-notifs-read': {
|
||||
|
||||
52
common/src/filters.ts
Normal file
52
common/src/filters.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {Lover, LoverRow} from "common/love/lover";
|
||||
import {cloneDeep} from "lodash";
|
||||
import {filterDefined} from "common/util/array";
|
||||
|
||||
export type FilterFields = {
|
||||
orderBy: 'last_online_time' | 'created_time' | 'compatibility_score'
|
||||
geodbCityIds: string[] | null
|
||||
genders: string[]
|
||||
name: string | undefined
|
||||
} & Pick<
|
||||
LoverRow,
|
||||
| 'wants_kids_strength'
|
||||
| 'pref_relation_styles'
|
||||
| 'is_smoker'
|
||||
| 'has_kids'
|
||||
| 'pref_gender'
|
||||
| 'pref_age_min'
|
||||
| 'pref_age_max'
|
||||
>
|
||||
export const orderLovers = (
|
||||
lovers: Lover[],
|
||||
starredUserIds: string[] | undefined
|
||||
) => {
|
||||
if (!lovers) return
|
||||
|
||||
let s = cloneDeep(lovers)
|
||||
|
||||
if (starredUserIds) {
|
||||
s = filterDefined([
|
||||
...starredUserIds.map((id) => s.find((l) => l.user_id === id)),
|
||||
...s.filter((l) => !starredUserIds.includes(l.user_id)),
|
||||
])
|
||||
}
|
||||
|
||||
// s = alternateWomenAndMen(s)
|
||||
|
||||
return s
|
||||
}
|
||||
export const initialFilters: Partial<FilterFields> = {
|
||||
geodbCityIds: undefined,
|
||||
name: undefined,
|
||||
genders: undefined,
|
||||
pref_age_max: undefined,
|
||||
pref_age_min: undefined,
|
||||
has_kids: undefined,
|
||||
wants_kids_strength: undefined,
|
||||
is_smoker: undefined,
|
||||
pref_relation_styles: undefined,
|
||||
pref_gender: undefined,
|
||||
orderBy: 'created_time',
|
||||
}
|
||||
export type OriginLocation = { id: string; name: string }
|
||||
51
common/src/has-kids.ts
Normal file
51
common/src/has-kids.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface HasKidLabel {
|
||||
name: string
|
||||
shortName: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface HasKidsLabelsMap {
|
||||
[key: string]: HasKidLabel
|
||||
}
|
||||
|
||||
export const hasKidsLabels: HasKidsLabelsMap = {
|
||||
no_preference: {
|
||||
name: 'Any kids',
|
||||
shortName: 'Any kids',
|
||||
value: -1,
|
||||
},
|
||||
has_kids: {
|
||||
name: 'Has kids',
|
||||
shortName: 'Yes',
|
||||
value: 1,
|
||||
},
|
||||
doesnt_have_kids: {
|
||||
name: `Doesn't have kids`,
|
||||
shortName: 'No',
|
||||
value: 0,
|
||||
},
|
||||
}
|
||||
export const hasKidsNames = Object.values(hasKidsLabels).reduce<Record<number, string>>(
|
||||
(acc, {value, name}) => {
|
||||
acc[value] = name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
|
||||
export const generateChoicesMap = (
|
||||
labels: HasKidsLabelsMap
|
||||
): Record<string, number> => {
|
||||
return Object.values(labels).reduce(
|
||||
(acc: Record<string, number>, label: HasKidLabel) => {
|
||||
acc[label.shortName] = label.value
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
// export const NO_PREFERENCE_STRENGTH = -1
|
||||
// export const WANTS_KIDS_STRENGTH = 2
|
||||
// export const DOESNT_WANT_KIDS_STRENGTH = 0
|
||||
26
common/src/love/bookmarked_searches.ts
Normal file
26
common/src/love/bookmarked_searches.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface MatchPrivateUser {
|
||||
email: string
|
||||
notificationPreferences: any
|
||||
}
|
||||
|
||||
export interface MatchUser {
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface MatchesType {
|
||||
description: {
|
||||
filters: any; // You might want to replace 'any' with a more specific type
|
||||
location: any; // You might want to replace 'any' with a more specific type
|
||||
};
|
||||
matches: any[]; // You might want to replace 'any' with a more specific type
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface MatchesByUserType {
|
||||
[key: string]: {
|
||||
user: any;
|
||||
privateUser: any;
|
||||
matches: MatchesType[];
|
||||
}
|
||||
}
|
||||
87
common/src/searches.ts
Normal file
87
common/src/searches.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// Define nice labels for each key
|
||||
import {FilterFields, initialFilters} from "common/filters";
|
||||
import {wantsKidsNames} from "common/wants-kids";
|
||||
import {hasKidsNames} from "common/has-kids";
|
||||
|
||||
const filterLabels: Record<string, string> = {
|
||||
geodbCityIds: "",
|
||||
location: "",
|
||||
name: "Searching",
|
||||
genders: "",
|
||||
pref_age_max: "Max age",
|
||||
pref_age_min: "Min age",
|
||||
has_kids: "",
|
||||
wants_kids_strength: "Kids",
|
||||
is_smoker: "",
|
||||
pref_relation_styles: "Seeking",
|
||||
pref_gender: "",
|
||||
orderBy: "",
|
||||
}
|
||||
|
||||
export type locationType = {
|
||||
location: {
|
||||
name: string
|
||||
}
|
||||
radius: number
|
||||
}
|
||||
|
||||
|
||||
export function formatFilters(filters: Partial<FilterFields>, location: locationType | null): String[] | null {
|
||||
const entries: String[] = []
|
||||
|
||||
let ageEntry = null
|
||||
let ageMin: number | undefined | null = filters.pref_age_min
|
||||
if (ageMin == 18) ageMin = undefined
|
||||
let ageMax = filters.pref_age_max;
|
||||
if (ageMax == 99 || ageMax == 100) ageMax = undefined
|
||||
if (ageMin || ageMax) {
|
||||
let text: string = 'Age: '
|
||||
if (ageMin) text = `${text}${ageMin}`
|
||||
if (ageMax) {
|
||||
if (ageMin) {
|
||||
text = `${text}-${ageMax}`
|
||||
} else {
|
||||
text = `${text}up to ${ageMax}`
|
||||
}
|
||||
} else {
|
||||
text = `${text}+`
|
||||
}
|
||||
ageEntry = text
|
||||
}
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
const typedKey = key as keyof FilterFields
|
||||
|
||||
if (value === undefined || value === null) return
|
||||
if (typedKey == 'pref_age_min' || typedKey == 'pref_age_max' || typedKey == 'geodbCityIds' || typedKey == 'orderBy') return
|
||||
if (Array.isArray(value) && value.length === 0) return
|
||||
if (initialFilters[typedKey] === value) return
|
||||
|
||||
const label = filterLabels[typedKey] ?? key
|
||||
|
||||
let stringValue = value
|
||||
if (key === 'has_kids') stringValue = hasKidsNames[value as number]
|
||||
if (key === 'wants_kids_strength') stringValue = wantsKidsNames[value as number]
|
||||
if (Array.isArray(value)) stringValue = value.join(', ')
|
||||
|
||||
if (!label) {
|
||||
const str = String(stringValue)
|
||||
stringValue = str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
const display = stringValue
|
||||
|
||||
entries.push(`${label}${label ? ': ' : ''}${display}`)
|
||||
})
|
||||
|
||||
if (ageEntry) entries.push(ageEntry)
|
||||
|
||||
if (location?.location?.name) {
|
||||
const locString = `${location?.location?.name} (${location?.radius}mi)`
|
||||
entries.push(locString)
|
||||
}
|
||||
|
||||
if (entries.length === 0) return ['Anyone']
|
||||
|
||||
return entries
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export type notification_preferences = {
|
||||
new_endorsement: notification_destination_types[]
|
||||
new_love_like: notification_destination_types[]
|
||||
new_love_ship: notification_destination_types[]
|
||||
new_search_alerts: notification_destination_types[]
|
||||
|
||||
// User-related
|
||||
new_message: notification_destination_types[]
|
||||
@@ -37,6 +38,7 @@ export const getDefaultNotificationPreferences = (isDev?: boolean) => {
|
||||
}
|
||||
const defaults: notification_preferences = {
|
||||
new_match: constructPref(true, true, true),
|
||||
new_search_alerts: constructPref(true, true, true),
|
||||
new_endorsement: constructPref(true, true, true),
|
||||
new_love_like: constructPref(true, false, false),
|
||||
new_love_ship: constructPref(true, false, false),
|
||||
@@ -59,8 +61,10 @@ export const getNotificationDestinationsForUser = (
|
||||
privateUser: PrivateUser,
|
||||
type: notification_preference
|
||||
) => {
|
||||
const destinations = privateUser.notificationPreferences[type]
|
||||
const opt_out = privateUser.notificationPreferences.opt_out_all
|
||||
let destinations = privateUser.notificationPreferences[type]
|
||||
if (!destinations) destinations = ['email', 'browser', 'mobile']
|
||||
let opt_out = privateUser.notificationPreferences.opt_out_all
|
||||
if (!opt_out) opt_out = []
|
||||
|
||||
return {
|
||||
sendToEmail: destinations.includes('email') && !opt_out.includes('email'),
|
||||
|
||||
68
common/src/wants-kids.ts
Normal file
68
common/src/wants-kids.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {hasKidsLabels} from "common/has-kids";
|
||||
|
||||
export type KidLabel = {
|
||||
name: string
|
||||
shortName: string
|
||||
strength: number
|
||||
}
|
||||
|
||||
export type KidsLabelsMap = Record<string, KidLabel>
|
||||
|
||||
export const wantsKidsLabels: KidsLabelsMap = {
|
||||
no_preference: {
|
||||
name: 'Any preference',
|
||||
shortName: 'Either',
|
||||
strength: -1,
|
||||
},
|
||||
wants_kids: {
|
||||
name: 'Wants kids',
|
||||
shortName: 'Yes',
|
||||
strength: 2,
|
||||
},
|
||||
doesnt_want_kids: {
|
||||
name: `Doesn't want kids`,
|
||||
shortName: 'No',
|
||||
strength: 0,
|
||||
},
|
||||
}
|
||||
export const wantsKidsNames = Object.values(wantsKidsLabels).reduce<Record<number, string>>(
|
||||
(acc, {strength, name}) => {
|
||||
acc[strength] = name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
export type wantsKidsDatabase = 0 | 1 | 2 | 3 | 4
|
||||
|
||||
export function wantsKidsToHasKidsFilter(wantsKidsStrength: wantsKidsDatabase) {
|
||||
if (wantsKidsStrength < wantsKidsLabels.wants_kids.strength) {
|
||||
return hasKidsLabels.doesnt_have_kids.value
|
||||
}
|
||||
return hasKidsLabels.no_preference.value
|
||||
}
|
||||
|
||||
export function wantsKidsDatabaseToWantsKidsFilter(
|
||||
wantsKidsStrength: wantsKidsDatabase
|
||||
) {
|
||||
// console.log(wantsKidsStrength)
|
||||
if (wantsKidsStrength == wantsKidsLabels.no_preference.strength) {
|
||||
return wantsKidsLabels.no_preference.strength
|
||||
}
|
||||
if (wantsKidsStrength > wantsKidsLabels.wants_kids.strength) {
|
||||
return wantsKidsLabels.wants_kids.strength
|
||||
}
|
||||
if (wantsKidsStrength < wantsKidsLabels.wants_kids.strength) {
|
||||
return wantsKidsLabels.doesnt_want_kids.strength
|
||||
}
|
||||
return wantsKidsLabels.no_preference.strength
|
||||
}
|
||||
|
||||
export const generateChoicesMap = (labels: KidsLabelsMap): Record<string, number> => {
|
||||
return Object.values(labels).reduce(
|
||||
(acc: Record<string, number>, label: KidLabel) => {
|
||||
acc[label.shortName] = label.strength
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx'
|
||||
import { FilterFields } from './search'
|
||||
import { RangeSlider } from 'web/components/widgets/slider'
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
export const PREF_AGE_MIN = 18
|
||||
export const PREF_AGE_MAX = 100
|
||||
|
||||
@@ -19,11 +19,12 @@ import {
|
||||
RelationshipFilter,
|
||||
RelationshipFilterText,
|
||||
} from './relationship-filter'
|
||||
import { FilterFields } from './search'
|
||||
import { KidsLabel, wantsKidsLabels } from './wants-kids-filter'
|
||||
import { HasKidsLabel, hasKidsLabels } from './has-kids-filter'
|
||||
import { KidsLabel, wantsKidsLabelsWithIcon } from './wants-kids-filter'
|
||||
import { HasKidsLabel } from './has-kids-filter'
|
||||
import { MyMatchesToggle } from './my-matches-toggle'
|
||||
import { Lover } from 'common/love/lover'
|
||||
import {FilterFields} from "common/filters";
|
||||
import {hasKidsLabels} from "common/has-kids";
|
||||
|
||||
export function DesktopFilters(props: {
|
||||
filters: Partial<FilterFields>
|
||||
|
||||
@@ -3,7 +3,8 @@ import GenderIcon from '../gender-icon'
|
||||
import { Gender } from 'common/gender'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { MultiCheckbox } from 'web/components/multi-checkbox'
|
||||
import { FilterFields } from './search'
|
||||
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
export function GenderFilterText(props: {
|
||||
gender: Gender[] | undefined
|
||||
|
||||
@@ -1,70 +1,20 @@
|
||||
import clsx from 'clsx'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { ChoicesToggleGroup } from 'web/components/widgets/choices-toggle-group'
|
||||
import { FilterFields } from './search'
|
||||
import { FaChild } from 'react-icons/fa6'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {ChoicesToggleGroup} from 'web/components/widgets/choices-toggle-group'
|
||||
import {FaChild} from 'react-icons/fa6'
|
||||
import {FilterFields} from "common/filters";
|
||||
import {generateChoicesMap, hasKidsLabels} from "common/has-kids";
|
||||
|
||||
export const NO_PREFERENCE_STRENGTH = -1
|
||||
export const WANTS_KIDS_STRENGTH = 2
|
||||
export const DOESNT_WANT_KIDS_STRENGTH = 0
|
||||
|
||||
interface HasKidLabel {
|
||||
name: string
|
||||
shortName: string
|
||||
value: number
|
||||
}
|
||||
|
||||
interface HasKidsLabelsMap {
|
||||
[key: string]: HasKidLabel
|
||||
}
|
||||
|
||||
export const hasKidsLabels: HasKidsLabelsMap = {
|
||||
no_preference: {
|
||||
name: 'Any kids',
|
||||
shortName: 'Any kids',
|
||||
value: -1,
|
||||
},
|
||||
has_kids: {
|
||||
name: 'Has kids',
|
||||
shortName: 'Yes',
|
||||
value: 1,
|
||||
},
|
||||
doesnt_have_kids: {
|
||||
name: `Doesn't have kids`,
|
||||
shortName: 'No',
|
||||
value: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export const hasKidsNames = Object.values(hasKidsLabels).reduce<Record<number, string>>(
|
||||
(acc, { value, name }) => {
|
||||
acc[value] = name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const generateChoicesMap = (
|
||||
labels: HasKidsLabelsMap
|
||||
): Record<string, number> => {
|
||||
return Object.values(labels).reduce(
|
||||
(acc: Record<string, number>, label: HasKidLabel) => {
|
||||
acc[label.shortName] = label.value
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
export function HasKidsLabel(props: {
|
||||
has_kids: number
|
||||
highlightedClass?: string
|
||||
mobile?: boolean
|
||||
}) {
|
||||
const { has_kids, highlightedClass, mobile } = props
|
||||
const {has_kids, highlightedClass, mobile} = props
|
||||
return (
|
||||
<Row className="items-center gap-0.5">
|
||||
<FaChild className="hidden h-4 w-4 sm:inline" />
|
||||
<FaChild className="hidden h-4 w-4 sm:inline"/>
|
||||
<span
|
||||
className={clsx(highlightedClass, has_kids !== -1 && 'font-semibold')}
|
||||
>
|
||||
@@ -73,12 +23,12 @@ export function HasKidsLabel(props: {
|
||||
? hasKidsLabels.has_kids.shortName
|
||||
: hasKidsLabels.has_kids.name
|
||||
: has_kids == hasKidsLabels.doesnt_have_kids.value
|
||||
? mobile
|
||||
? hasKidsLabels.doesnt_have_kids.shortName
|
||||
: hasKidsLabels.doesnt_have_kids.name
|
||||
: mobile
|
||||
? hasKidsLabels.no_preference.shortName
|
||||
: hasKidsLabels.no_preference.name}
|
||||
? mobile
|
||||
? hasKidsLabels.doesnt_have_kids.shortName
|
||||
: hasKidsLabels.doesnt_have_kids.name
|
||||
: mobile
|
||||
? hasKidsLabels.no_preference.shortName
|
||||
: hasKidsLabels.no_preference.name}
|
||||
</span>
|
||||
</Row>
|
||||
)
|
||||
@@ -88,12 +38,12 @@ export function HasKidsFilter(props: {
|
||||
filters: Partial<FilterFields>
|
||||
updateFilter: (newState: Partial<FilterFields>) => void
|
||||
}) {
|
||||
const { filters, updateFilter } = props
|
||||
const {filters, updateFilter} = props
|
||||
return (
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={filters.has_kids ?? 0}
|
||||
choicesMap={generateChoicesMap(hasKidsLabels)}
|
||||
setChoice={(c) => updateFilter({ has_kids: Number(c) })}
|
||||
setChoice={(c) => updateFilter({has_kids: Number(c)})}
|
||||
toggleClassName="w-1/3 justify-center"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import clsx from 'clsx'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Slider } from 'web/components/widgets/slider'
|
||||
import { usePersistentInMemoryState } from 'web/hooks/use-persistent-in-memory-state'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import {
|
||||
originToCity,
|
||||
City,
|
||||
CityRow,
|
||||
useCitySearch,
|
||||
loverToCity,
|
||||
} from '../search-location'
|
||||
import { Lover } from 'common/love/lover'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Input } from 'web/components/widgets/input'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
import { uniqBy } from 'lodash'
|
||||
import { buildArray } from 'common/util/array'
|
||||
|
||||
export const PREF_AGE_MIN = 18
|
||||
export const PREF_AGE_MAX = 100
|
||||
|
||||
export type OriginLocation = { id: string; name: string }
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Slider} from 'web/components/widgets/slider'
|
||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {City, CityRow, loverToCity, originToCity, useCitySearch,} from '../search-location'
|
||||
import {Lover} from 'common/love/lover'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Input} from 'web/components/widgets/input'
|
||||
import {XIcon} from '@heroicons/react/solid'
|
||||
import {uniqBy} from 'lodash'
|
||||
import {buildArray} from 'common/util/array'
|
||||
import {OriginLocation} from "common/filters";
|
||||
|
||||
export function LocationFilterText(props: {
|
||||
location: OriginLocation | undefined | null
|
||||
@@ -28,7 +18,7 @@ export function LocationFilterText(props: {
|
||||
radius: number
|
||||
highlightedClass?: string
|
||||
}) {
|
||||
const { location, youLover, radius, highlightedClass } = props
|
||||
const { location, radius, highlightedClass } = props
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Col } from 'web/components/layout/col'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { AgeFilter, AgeFilterText, getNoMinMaxAge } from './age-filter'
|
||||
import { GenderFilter, GenderFilterText } from './gender-filter'
|
||||
import { HasKidsFilter, HasKidsLabel, hasKidsLabels } from './has-kids-filter'
|
||||
import { HasKidsFilter, HasKidsLabel } from './has-kids-filter'
|
||||
import {
|
||||
LocationFilter,
|
||||
LocationFilterProps,
|
||||
@@ -16,18 +16,19 @@ import {
|
||||
RelationshipFilter,
|
||||
RelationshipFilterText,
|
||||
} from './relationship-filter'
|
||||
import { FilterFields } from './search'
|
||||
import {
|
||||
KidsLabel,
|
||||
WantsKidsFilter,
|
||||
WantsKidsIcon,
|
||||
wantsKidsLabels,
|
||||
wantsKidsLabelsWithIcon,
|
||||
} from './wants-kids-filter'
|
||||
import { FaChild } from 'react-icons/fa6'
|
||||
import { MyMatchesToggle } from './my-matches-toggle'
|
||||
import { Lover } from 'common/love/lover'
|
||||
import { Gender } from 'common/gender'
|
||||
import { RelationshipType } from 'web/lib/util/convert-relationship-type'
|
||||
import {FilterFields} from "common/filters";
|
||||
import {hasKidsLabels} from "common/has-kids";
|
||||
|
||||
export function MobileFilters(props: {
|
||||
filters: Partial<FilterFields>
|
||||
|
||||
@@ -3,7 +3,8 @@ import GenderIcon from '../gender-icon'
|
||||
import { Gender } from 'common/gender'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { MultiCheckbox } from 'web/components/multi-checkbox'
|
||||
import { FilterFields } from './search'
|
||||
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
export function PrefGenderFilterText(props: {
|
||||
pref_gender: Gender[] | undefined
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import clsx from 'clsx'
|
||||
import {convertRelationshipType, RelationshipType,} from 'web/lib/util/convert-relationship-type'
|
||||
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
|
||||
import {FilterFields} from './search'
|
||||
import {MultiCheckbox} from 'web/components/multi-checkbox'
|
||||
|
||||
import {RELATIONSHIP_CHOICES} from "web/components/filters/choices";
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
export function RelationshipFilterText(props: {
|
||||
relationship: RelationshipType[] | undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Lover, LoverRow} from 'common/love/lover'
|
||||
import {Lover} from 'common/love/lover'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {IoFilterSharp} from 'react-icons/io5'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
@@ -15,24 +15,8 @@ import {BookmarkedSearchesType} from "web/hooks/use-bookmarked-searches";
|
||||
import {submitBookmarkedSearch} from "web/lib/supabase/searches";
|
||||
import {useUser} from "web/hooks/use-user";
|
||||
import {isEqual} from "lodash";
|
||||
import {initialFilters} from "web/components/filters/use-filters";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export type FilterFields = {
|
||||
orderBy: 'last_online_time' | 'created_time' | 'compatibility_score'
|
||||
geodbCityIds: string[] | null
|
||||
genders: string[]
|
||||
name: string | undefined
|
||||
} & Pick<
|
||||
LoverRow,
|
||||
| 'wants_kids_strength'
|
||||
| 'pref_relation_styles'
|
||||
| 'is_smoker'
|
||||
| 'has_kids'
|
||||
| 'pref_gender'
|
||||
| 'pref_age_min'
|
||||
| 'pref_age_max'
|
||||
>
|
||||
import {FilterFields, initialFilters} from "common/filters";
|
||||
|
||||
function isOrderBy(input: string): input is FilterFields['orderBy'] {
|
||||
return ['last_online_time', 'created_time', 'compatibility_score'].includes(
|
||||
|
||||
@@ -1,88 +1,43 @@
|
||||
import { Lover } from 'common/love/lover'
|
||||
import { filterDefined } from 'common/util/array'
|
||||
import { cloneDeep, debounce, isEqual } from 'lodash'
|
||||
import { useCallback } from 'react'
|
||||
import { useEffectCheckEquality } from 'web/hooks/use-effect-check-equality'
|
||||
import { useIsLooking } from 'web/hooks/use-is-looking'
|
||||
import { useNearbyCities } from 'web/hooks/use-nearby-locations'
|
||||
import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'
|
||||
import { OriginLocation } from './location-filter'
|
||||
import { FilterFields } from './search'
|
||||
import {
|
||||
wantsKidsDatabaseToWantsKidsFilter,
|
||||
wantsKidsDatabase,
|
||||
wantsKidsToHasKidsFilter,
|
||||
} from './wants-kids-filter'
|
||||
|
||||
export const orderLovers = (
|
||||
lovers: Lover[],
|
||||
starredUserIds: string[] | undefined
|
||||
) => {
|
||||
if (!lovers) return
|
||||
|
||||
let s = cloneDeep(lovers)
|
||||
|
||||
if (starredUserIds) {
|
||||
s = filterDefined([
|
||||
...starredUserIds.map((id) => s.find((l) => l.user_id === id)),
|
||||
...s.filter((l) => !starredUserIds.includes(l.user_id)),
|
||||
])
|
||||
}
|
||||
|
||||
// s = alternateWomenAndMen(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// const alternateWomenAndMen = (lovers: Lover[]) => {
|
||||
// const [women, nonWomen] = partition(lovers, (l) => l.gender === 'female')
|
||||
// return filterDefined(zip(women, nonWomen).flat())
|
||||
// }
|
||||
|
||||
export const initialFilters: Partial<FilterFields> = {
|
||||
geodbCityIds: undefined,
|
||||
name: undefined,
|
||||
genders: undefined,
|
||||
pref_age_max: undefined,
|
||||
pref_age_min: undefined,
|
||||
has_kids: undefined,
|
||||
wants_kids_strength: undefined,
|
||||
is_smoker: undefined,
|
||||
pref_relation_styles: undefined,
|
||||
pref_gender: undefined,
|
||||
orderBy: 'created_time',
|
||||
}
|
||||
import {Lover} from "common/love/lover";
|
||||
import {useIsLooking} from "web/hooks/use-is-looking";
|
||||
import {usePersistentLocalState} from "web/hooks/use-persistent-local-state";
|
||||
import {useCallback} from "react";
|
||||
import {debounce, isEqual} from "lodash";
|
||||
import {useNearbyCities} from "web/hooks/use-nearby-locations";
|
||||
import {useEffectCheckEquality} from "web/hooks/use-effect-check-equality";
|
||||
import {wantsKidsDatabase, wantsKidsDatabaseToWantsKidsFilter, wantsKidsToHasKidsFilter} from "common/wants-kids";
|
||||
import {FilterFields, initialFilters, OriginLocation} from "common/filters";
|
||||
|
||||
export const useFilters = (you: Lover | undefined) => {
|
||||
const isLooking = useIsLooking()
|
||||
const [filters, setFilters] = usePersistentLocalState<Partial<FilterFields>>(
|
||||
isLooking ? initialFilters : { ...initialFilters, orderBy: 'created_time' },
|
||||
isLooking ? initialFilters : {...initialFilters, orderBy: 'created_time'},
|
||||
'profile-filters-2'
|
||||
)
|
||||
|
||||
const updateFilter = (newState: Partial<FilterFields>) => {
|
||||
const updatedState = { ...newState }
|
||||
|
||||
const updatedState = {...newState}
|
||||
|
||||
if ('pref_age_min' in updatedState && updatedState.pref_age_min !== undefined) {
|
||||
if (updatedState.pref_age_min != null && updatedState.pref_age_min <= 18) {
|
||||
updatedState.pref_age_min = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ('pref_age_max' in updatedState && updatedState.pref_age_max !== undefined) {
|
||||
if (updatedState.pref_age_max != null && updatedState.pref_age_max >= 99) {
|
||||
updatedState.pref_age_max = undefined
|
||||
}
|
||||
}
|
||||
|
||||
setFilters((prevState) => ({ ...prevState, ...updatedState }))
|
||||
|
||||
setFilters((prevState) => ({...prevState, ...updatedState}))
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters(
|
||||
isLooking
|
||||
? initialFilters
|
||||
: { ...initialFilters, orderBy: 'created_time' }
|
||||
: {...initialFilters, orderBy: 'created_time'}
|
||||
)
|
||||
setLocation(undefined)
|
||||
}
|
||||
@@ -101,7 +56,7 @@ export const useFilters = (you: Lover | undefined) => {
|
||||
const nearbyCities = useNearbyCities(location?.id, radius)
|
||||
|
||||
useEffectCheckEquality(() => {
|
||||
updateFilter({ geodbCityIds: nearbyCities })
|
||||
updateFilter({geodbCityIds: nearbyCities})
|
||||
}, [nearbyCities])
|
||||
|
||||
const locationFilterProps = {
|
||||
@@ -144,7 +99,7 @@ export const useFilters = (you: Lover | undefined) => {
|
||||
setRadius(100)
|
||||
debouncedSetRadius(100) // clear any pending debounced sets
|
||||
if (you?.geodb_city_id && you.city) {
|
||||
setLocation({ id: you?.geodb_city_id, name: you?.city })
|
||||
setLocation({id: you?.geodb_city_id, name: you?.city})
|
||||
}
|
||||
} else {
|
||||
clearFilters()
|
||||
@@ -160,3 +115,8 @@ export const useFilters = (you: Lover | undefined) => {
|
||||
locationFilterProps,
|
||||
}
|
||||
}
|
||||
|
||||
// const alternateWomenAndMen = (lovers: Lover[]) => {
|
||||
// const [women, nonWomen] = partition(lovers, (l) => l.gender === 'female')
|
||||
// return filterDefined(zip(women, nonWomen).flat())
|
||||
// }
|
||||
@@ -2,94 +2,45 @@ import {ReactNode} from 'react'
|
||||
import {MdNoStroller, MdOutlineStroller, MdStroller} from 'react-icons/md'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {ChoicesToggleGroup} from 'web/components/widgets/choices-toggle-group'
|
||||
import {FilterFields} from './search'
|
||||
import {hasKidsLabels} from './has-kids-filter'
|
||||
import clsx from 'clsx'
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
interface KidLabel {
|
||||
name: string
|
||||
shortName: string
|
||||
import {generateChoicesMap, KidLabel, wantsKidsLabels} from "common/wants-kids"
|
||||
|
||||
interface KidLabelWithIcon extends KidLabel {
|
||||
icon: ReactNode
|
||||
strength: number
|
||||
}
|
||||
|
||||
interface KidsLabelsMap {
|
||||
[key: string]: KidLabel
|
||||
interface KidsLabelsMapWithIcon {
|
||||
[key: string]: KidLabelWithIcon
|
||||
}
|
||||
|
||||
export type wantsKidsDatabase = 0 | 1 | 2 | 3 | 4
|
||||
|
||||
export function wantsKidsToHasKidsFilter(wantsKidsStrength: wantsKidsDatabase) {
|
||||
if (wantsKidsStrength < wantsKidsLabels.wants_kids.strength) {
|
||||
return hasKidsLabels.doesnt_have_kids.value
|
||||
}
|
||||
return hasKidsLabels.no_preference.value
|
||||
}
|
||||
|
||||
export function wantsKidsDatabaseToWantsKidsFilter(
|
||||
wantsKidsStrength: wantsKidsDatabase
|
||||
) {
|
||||
console.log(wantsKidsStrength)
|
||||
if (wantsKidsStrength == wantsKidsLabels.no_preference.strength) {
|
||||
return wantsKidsLabels.no_preference.strength
|
||||
}
|
||||
if (wantsKidsStrength > wantsKidsLabels.wants_kids.strength) {
|
||||
return wantsKidsLabels.wants_kids.strength
|
||||
}
|
||||
if (wantsKidsStrength < wantsKidsLabels.wants_kids.strength) {
|
||||
return wantsKidsLabels.doesnt_want_kids.strength
|
||||
}
|
||||
return wantsKidsLabels.no_preference.strength
|
||||
}
|
||||
|
||||
export const wantsKidsLabels: KidsLabelsMap = {
|
||||
export const wantsKidsLabelsWithIcon: KidsLabelsMapWithIcon = {
|
||||
...wantsKidsLabels,
|
||||
no_preference: {
|
||||
name: 'Any preference',
|
||||
shortName: 'Either',
|
||||
...wantsKidsLabels.no_preference,
|
||||
icon: <MdOutlineStroller className="h-4 w-4"/>,
|
||||
strength: -1,
|
||||
},
|
||||
wants_kids: {
|
||||
name: 'Wants kids',
|
||||
shortName: 'Yes',
|
||||
...wantsKidsLabels.wants_kids,
|
||||
icon: <MdStroller className="h-4 w-4"/>,
|
||||
strength: 2,
|
||||
},
|
||||
doesnt_want_kids: {
|
||||
name: `Doesn't want kids`,
|
||||
shortName: 'No',
|
||||
...wantsKidsLabels.doesnt_want_kids,
|
||||
icon: <MdNoStroller className="h-4 w-4"/>,
|
||||
strength: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export const wantsKidsNames = Object.values(wantsKidsLabels).reduce<Record<number, string>>(
|
||||
(acc, {strength, name}) => {
|
||||
acc[strength] = name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const generateChoicesMap = (labels: KidsLabelsMap): Record<string, number> => {
|
||||
return Object.values(labels).reduce(
|
||||
(acc: Record<string, number>, label: KidLabel) => {
|
||||
acc[label.shortName] = label.strength
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
export function WantsKidsIcon(props: { strength: number; className?: string }) {
|
||||
const {strength, className} = props
|
||||
return (
|
||||
<span className={className}>
|
||||
{strength == wantsKidsLabels.no_preference.strength
|
||||
? wantsKidsLabels.no_preference.icon
|
||||
: strength == wantsKidsLabels.wants_kids.strength
|
||||
? wantsKidsLabels.wants_kids.icon
|
||||
: wantsKidsLabels.doesnt_want_kids.icon}
|
||||
{strength == wantsKidsLabelsWithIcon.no_preference.strength
|
||||
? wantsKidsLabelsWithIcon.no_preference.icon
|
||||
: strength == wantsKidsLabelsWithIcon.wants_kids.strength
|
||||
? wantsKidsLabelsWithIcon.wants_kids.icon
|
||||
: wantsKidsLabelsWithIcon.doesnt_want_kids.icon}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -106,21 +57,21 @@ export function KidsLabel(props: {
|
||||
<WantsKidsIcon strength={strength} className={clsx('hidden sm:inline')}/>
|
||||
<span
|
||||
className={clsx(
|
||||
strength != wantsKidsLabels.no_preference.strength && 'font-semibold',
|
||||
strength != wantsKidsLabelsWithIcon.no_preference.strength && 'font-semibold',
|
||||
highlightedClass
|
||||
)}
|
||||
>
|
||||
{strength == wantsKidsLabels.no_preference.strength
|
||||
{strength == wantsKidsLabelsWithIcon.no_preference.strength
|
||||
? mobile
|
||||
? wantsKidsLabels.no_preference.shortName
|
||||
: wantsKidsLabels.no_preference.name
|
||||
: strength == wantsKidsLabels.wants_kids.strength
|
||||
? wantsKidsLabelsWithIcon.no_preference.shortName
|
||||
: wantsKidsLabelsWithIcon.no_preference.name
|
||||
: strength == wantsKidsLabelsWithIcon.wants_kids.strength
|
||||
? mobile
|
||||
? wantsKidsLabels.wants_kids.shortName
|
||||
: wantsKidsLabels.wants_kids.name
|
||||
? wantsKidsLabelsWithIcon.wants_kids.shortName
|
||||
: wantsKidsLabelsWithIcon.wants_kids.name
|
||||
: mobile
|
||||
? wantsKidsLabels.doesnt_want_kids.shortName
|
||||
: wantsKidsLabels.doesnt_want_kids.name}
|
||||
? wantsKidsLabelsWithIcon.doesnt_want_kids.shortName
|
||||
: wantsKidsLabelsWithIcon.doesnt_want_kids.name}
|
||||
</span>
|
||||
</Row>
|
||||
)
|
||||
@@ -135,7 +86,7 @@ export function WantsKidsFilter(props: {
|
||||
return (
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={filters.wants_kids_strength ?? 0}
|
||||
choicesMap={generateChoicesMap(wantsKidsLabels)}
|
||||
choicesMap={generateChoicesMap(wantsKidsLabelsWithIcon)}
|
||||
setChoice={(c) => updateFilter({wants_kids_strength: Number(c)})}
|
||||
toggleClassName="w-1/3 justify-center"
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {getStars} from 'web/lib/supabase/stars'
|
||||
import Router from 'next/router'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {orderLovers, useFilters} from 'web/components/filters/use-filters'
|
||||
import {ProfileGrid} from 'web/components/profile-grid'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
@@ -16,6 +15,8 @@ import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-sta
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {api} from 'web/lib/api'
|
||||
import {useBookmarkedSearches} from "web/hooks/use-bookmarked-searches";
|
||||
import {orderLovers} from "common/filters";
|
||||
import {useFilters} from "web/components/filters/use-filters";
|
||||
|
||||
export function ProfilesHome() {
|
||||
const user = useUser();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { OriginLocation } from './filters/location-filter'
|
||||
import { api } from 'web/lib/api'
|
||||
import { countryCodeToFlag } from 'web/lib/util/location'
|
||||
import { LoverRow } from 'common/love/lover'
|
||||
import {OriginLocation} from "common/filters";
|
||||
|
||||
export type City = {
|
||||
geodb_city_id: string
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {User} from "common/user";
|
||||
import {ReactElement} from "react";
|
||||
import {Button} from "web/components/buttons/button";
|
||||
import {Modal, MODAL_CLASS} from "web/components/layout/modal";
|
||||
import {Col} from "web/components/layout/col";
|
||||
import {BookmarkedSearchesType} from "web/hooks/use-bookmarked-searches";
|
||||
import {useUser} from "web/hooks/use-user";
|
||||
import {initialFilters} from "web/components/filters/use-filters";
|
||||
import {deleteBookmarkedSearch} from "web/lib/supabase/searches";
|
||||
import {FilterFields} from "web/components/filters/search";
|
||||
import {hasKidsNames} from "web/components/filters/has-kids-filter";
|
||||
import {wantsKidsNames} from "web/components/filters/wants-kids-filter";
|
||||
import {formatFilters, locationType} from "common/searches";
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
export function BookmarkSearchButton(props: {
|
||||
bookmarkedSearches: BookmarkedSearchesType[]
|
||||
@@ -42,107 +39,6 @@ export function BookmarkSearchButton(props: {
|
||||
)
|
||||
}
|
||||
|
||||
// Define nice labels for each key
|
||||
const filterLabels: Record<string, string> = {
|
||||
geodbCityIds: "",
|
||||
location: "",
|
||||
name: "Searching",
|
||||
genders: "",
|
||||
pref_age_max: "Max age",
|
||||
pref_age_min: "Min age",
|
||||
has_kids: "",
|
||||
wants_kids_strength: "Kids",
|
||||
is_smoker: "",
|
||||
pref_relation_styles: "Seeking",
|
||||
pref_gender: "",
|
||||
orderBy: "",
|
||||
}
|
||||
|
||||
export type locationType = {
|
||||
location: {
|
||||
name: string
|
||||
}
|
||||
radius: number
|
||||
}
|
||||
|
||||
|
||||
function formatFilters(filters: Partial<FilterFields>, location: locationType | null): ReactElement | null {
|
||||
const entries: ReactElement[] = []
|
||||
|
||||
let ageEntry = null
|
||||
let ageMin: number | undefined | null = filters.pref_age_min
|
||||
if (ageMin == 18) ageMin = undefined
|
||||
let ageMax = filters.pref_age_max;
|
||||
if (ageMax == 99 || ageMax == 100) ageMax = undefined
|
||||
if (ageMin || ageMax) {
|
||||
let text: string = 'Age: '
|
||||
if (ageMin) text = `${text}${ageMin}`
|
||||
if (ageMax) {
|
||||
if (ageMin) {
|
||||
text = `${text}-${ageMax}`
|
||||
} else {
|
||||
text = `${text}up to ${ageMax}`
|
||||
}
|
||||
} else {
|
||||
text = `${text}+`
|
||||
}
|
||||
ageEntry = <span key='age'>{text}</span>
|
||||
}
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
const typedKey = key as keyof FilterFields
|
||||
|
||||
if (value === undefined || value === null) return
|
||||
if (typedKey == 'pref_age_min' || typedKey == 'pref_age_max' || typedKey == 'geodbCityIds' || typedKey == 'orderBy') return
|
||||
if (Array.isArray(value) && value.length === 0) return
|
||||
if (initialFilters[typedKey] === value) return
|
||||
|
||||
const label = filterLabels[typedKey] ?? key
|
||||
|
||||
let stringValue = value
|
||||
if (key === 'has_kids') stringValue = hasKidsNames[value as number]
|
||||
if (key === 'wants_kids_strength') stringValue = wantsKidsNames[value as number]
|
||||
if (Array.isArray(value)) stringValue = value.join(', ')
|
||||
|
||||
if (!label) {
|
||||
const str = String(stringValue)
|
||||
stringValue = str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
const display: ReactElement = key === 'name'
|
||||
? <i>{stringValue as string}</i>
|
||||
: <>{stringValue}</>
|
||||
|
||||
entries.push(
|
||||
<span key={key}>
|
||||
{label}
|
||||
{label ? ': ' : ''}
|
||||
{display}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
if (ageEntry) entries.push(ageEntry)
|
||||
|
||||
if (location?.location?.name) {
|
||||
const locString = `${location?.location?.name} (${location?.radius}mi)`
|
||||
entries.push(<span key="location">{locString}</span>)
|
||||
}
|
||||
|
||||
if (entries.length === 0) return <span>Anything</span>
|
||||
|
||||
// Join with " • " separators
|
||||
return (
|
||||
<span>
|
||||
{entries.map((entry, i) => (
|
||||
<span key={i}>
|
||||
{entry}
|
||||
{i < entries.length - 1 ? ' • ' : ''}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ButtonModal(props: {
|
||||
open: boolean
|
||||
@@ -172,7 +68,7 @@ function ButtonModal(props: {
|
||||
{(bookmarkedSearches || []).map((search) => (
|
||||
<li key={search.id}
|
||||
className="items-center justify-between gap-2 list-item marker:text-ink-500 marker:font-bold">
|
||||
{formatFilters(search.search_filters as Partial<FilterFields>, search.location as locationType)}
|
||||
{formatFilters(search.search_filters as Partial<FilterFields>, search.location as locationType)?.join(" • ")}
|
||||
<button
|
||||
onClick={async () => {
|
||||
await deleteBookmarkedSearch(search.id)
|
||||
|
||||
@@ -2,8 +2,8 @@ import {Row, run} from "common/supabase/utils";
|
||||
import {db} from "web/lib/supabase/db";
|
||||
import {filterKeys} from "web/components/questions-form";
|
||||
import {track} from "web/lib/service/analytics";
|
||||
import {FilterFields} from "web/components/filters/search";
|
||||
import {removeNullOrUndefinedProps} from "common/util/object";
|
||||
import {FilterFields} from "common/filters";
|
||||
|
||||
|
||||
export const getUserBookmarkedSearches = async (userId: string) => {
|
||||
|
||||
@@ -159,7 +159,7 @@ const LoadedNotificationSettings = (props: { privateUser: PrivateUser }) => {
|
||||
},
|
||||
{
|
||||
type: 'new_message',
|
||||
question: '... sends you a new messages?',
|
||||
question: '... sends you a new message?',
|
||||
},
|
||||
{
|
||||
type: 'new_love_like',
|
||||
@@ -181,7 +181,10 @@ const LoadedNotificationSettings = (props: { privateUser: PrivateUser }) => {
|
||||
type: 'on_new_follow',
|
||||
question: '... follows you?',
|
||||
},
|
||||
|
||||
{
|
||||
type: 'new_search_alerts',
|
||||
question: 'Alerts from bookmarked searches?',
|
||||
},
|
||||
{
|
||||
type: 'opt_out_all',
|
||||
question:
|
||||
|
||||
Reference in New Issue
Block a user