mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-20 07:44:01 -05:00
483 lines
15 KiB
TypeScript
483 lines
15 KiB
TypeScript
import clsx from 'clsx'
|
|
import {convertRace, type RelationshipType,} from 'web/lib/util/convert-types'
|
|
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
|
|
import React, {ReactNode} from 'react'
|
|
import {
|
|
INVERTED_DIET_CHOICES,
|
|
INVERTED_EDUCATION_CHOICES,
|
|
INVERTED_LANGUAGE_CHOICES,
|
|
INVERTED_MBTI_CHOICES,
|
|
INVERTED_POLITICAL_CHOICES,
|
|
INVERTED_RELATIONSHIP_STATUS_CHOICES,
|
|
INVERTED_RELIGION_CHOICES,
|
|
RELATIONSHIP_ICONS
|
|
} from 'web/components/filters/choices'
|
|
import {BiSolidDrink} from 'react-icons/bi'
|
|
import {BsPersonHeart, BsPersonVcard} from 'react-icons/bs'
|
|
import {FaChild} from 'react-icons/fa6'
|
|
import {LuBriefcase, LuCigarette, LuCigaretteOff, LuGraduationCap,} from 'react-icons/lu'
|
|
import {MdLanguage, MdNoDrinks, MdOutlineChildFriendly} from 'react-icons/md'
|
|
import {PiHandsPrayingBold, PiMagnifyingGlassBold,} from 'react-icons/pi'
|
|
import {RiScales3Line} from 'react-icons/ri'
|
|
import {Col} from 'web/components/layout/col'
|
|
import {Row} from 'web/components/layout/row'
|
|
import {fromNow} from 'web/lib/util/time'
|
|
import {convertGenderPlural, Gender} from 'common/gender'
|
|
import {HiOutlineGlobe} from 'react-icons/hi'
|
|
import {UserHandles} from 'web/components/user/user-handles'
|
|
import {Profile} from 'common/profiles/profile'
|
|
import {UserActivity} from "common/user";
|
|
import {ClockIcon} from "@heroicons/react/solid";
|
|
import {MAX_INT, MIN_INT} from "common/constants";
|
|
import {GiFruitBowl} from "react-icons/gi";
|
|
import {FaBriefcase, FaHandsHelping, FaHeart, FaStar} from "react-icons/fa";
|
|
import {useLocale, useT} from "web/lib/locale";
|
|
import {useChoices} from "web/hooks/use-choices";
|
|
import {getSeekingGenderText} from "web/lib/profile/seeking";
|
|
import {TbBulb, TbCheck, TbMoodSad, TbUsers} from "react-icons/tb";
|
|
|
|
export function AboutRow(props: {
|
|
icon: ReactNode
|
|
text?: string | null | string[]
|
|
preText?: string
|
|
suffix?: string | null
|
|
}) {
|
|
const {icon, text, preText, suffix} = props
|
|
const t = useT()
|
|
if (!text?.length && !preText && !suffix) {
|
|
return <></>
|
|
}
|
|
let formattedText = ''
|
|
if (preText) {
|
|
formattedText += preText
|
|
}
|
|
if (text?.length) {
|
|
formattedText += stringOrStringArrayToText({
|
|
text: text,
|
|
preText: preText,
|
|
asSentence: false,
|
|
capitalizeFirstLetterOption: true,
|
|
t: t,
|
|
})
|
|
}
|
|
if (suffix) {
|
|
formattedText += formattedText ? ` (${suffix})` : suffix
|
|
}
|
|
return (
|
|
<Row className="items-center gap-2">
|
|
<div className="text-ink-600 w-5">{icon}</div>
|
|
<div>
|
|
{formattedText}
|
|
</div>
|
|
</Row>
|
|
)
|
|
}
|
|
|
|
export default function ProfileAbout(props: {
|
|
profile: Profile,
|
|
userActivity?: UserActivity,
|
|
isCurrentUser: boolean,
|
|
}) {
|
|
const {profile, userActivity, isCurrentUser} = props
|
|
const t = useT()
|
|
const {choices: interestsById} = useChoices('interests')
|
|
const {choices: causesById} = useChoices('causes')
|
|
const {choices: workById} = useChoices('work')
|
|
const {locale} = useLocale()
|
|
|
|
return (
|
|
<Col
|
|
className={clsx('bg-canvas-0 relative gap-3 overflow-hidden rounded p-4')}
|
|
>
|
|
<Seeking profile={profile}/>
|
|
<RelationshipType profile={profile}/>
|
|
<RelationshipStatus profile={profile}/>
|
|
<Education profile={profile}/>
|
|
<Occupation profile={profile}/>
|
|
<AboutRow
|
|
icon={<FaBriefcase className="h-5 w-5"/>}
|
|
text={profile.work?.map(id => workById[id]).filter(Boolean).sort((a, b) => a.localeCompare(b, locale)) as string[]}
|
|
/>
|
|
<AboutRow
|
|
icon={<RiScales3Line className="h-5 w-5"/>}
|
|
text={profile.political_beliefs?.map(belief => t(`profile.political.${belief}`, INVERTED_POLITICAL_CHOICES[belief]))}
|
|
suffix={profile.political_details}
|
|
/>
|
|
<AboutRow
|
|
icon={<PiHandsPrayingBold className="h-5 w-5"/>}
|
|
text={profile.religion?.map(belief => t(`profile.religion.${belief}`, INVERTED_RELIGION_CHOICES[belief]))}
|
|
suffix={profile.religious_beliefs}
|
|
/>
|
|
<AboutRow
|
|
icon={<FaStar className="h-5 w-5"/>}
|
|
text={profile.interests?.map(id => interestsById[id]).filter(Boolean).sort((a, b) => a.localeCompare(b, locale)) as string[]}
|
|
/>
|
|
<AboutRow
|
|
icon={<FaHandsHelping className="h-5 w-5"/>}
|
|
text={profile.causes?.map(id => causesById[id]).filter(Boolean).sort((a, b) => a.localeCompare(b, locale)) as string[]}
|
|
/>
|
|
<AboutRow
|
|
icon={<BsPersonVcard className="h-5 w-5"/>}
|
|
text={profile.mbti ? INVERTED_MBTI_CHOICES[profile.mbti] : null}
|
|
/>
|
|
<Big5Traits profile={profile}/>
|
|
<AboutRow
|
|
icon={<HiOutlineGlobe className="h-5 w-5"/>}
|
|
text={profile.ethnicity
|
|
?.filter((r) => r !== 'other')
|
|
?.map((r: any) => t(`profile.race.${r}`, convertRace(r)))}
|
|
/>
|
|
<Smoker profile={profile}/>
|
|
<Drinks profile={profile}/>
|
|
<AboutRow
|
|
icon={<GiFruitBowl className="h-5 w-5"/>}
|
|
text={profile.diet?.map(e => t(`profile.diet.${e}`, INVERTED_DIET_CHOICES[e]))}
|
|
/>
|
|
<AboutRow
|
|
icon={<MdLanguage className="h-5 w-5"/>}
|
|
text={profile.languages?.map(v => t(`profile.language.${v}`, INVERTED_LANGUAGE_CHOICES[v]))}
|
|
/>
|
|
<HasKids profile={profile}/>
|
|
<WantsKids profile={profile}/>
|
|
{!isCurrentUser && <LastOnline lastOnlineTime={userActivity?.last_online_time}/>}
|
|
<UserHandles links={profile.user.link}/>
|
|
</Col>
|
|
)
|
|
}
|
|
|
|
function Seeking(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const prefGender = profile.pref_gender
|
|
const min = profile.pref_age_min
|
|
const max = profile.pref_age_max
|
|
const seekingGenderText = stringOrStringArrayToText({
|
|
text:
|
|
prefGender?.length == 5
|
|
? ['people']
|
|
: prefGender?.map((gender) => t(`profile.gender.plural.${gender}`, convertGenderPlural(gender as Gender)).toLowerCase()),
|
|
preText: t('profile.interested_in', 'Interested in'),
|
|
asSentence: true,
|
|
capitalizeFirstLetterOption: false,
|
|
t: t,
|
|
})
|
|
|
|
const noMin = (min ?? MIN_INT) <= 18
|
|
const noMax = (max ?? MAX_INT) >= 99
|
|
|
|
const ageRangeText =
|
|
noMin && noMax
|
|
? t('profile.age_any', 'of any age')
|
|
: min == max
|
|
? t('profile.age_exact', 'exactly {min} years old', {min})
|
|
: noMax
|
|
? t('profile.age_older_than', 'older than {min}', {min})
|
|
: noMin
|
|
? t('profile.age_younger_than', 'younger than {max}', {max})
|
|
: t('profile.age_between', 'between {min} - {max} years old', {min, max})
|
|
|
|
if (!prefGender || prefGender.length < 1) {
|
|
return <></>
|
|
}
|
|
return (
|
|
<AboutRow
|
|
icon={<PiMagnifyingGlassBold className="h-5 w-5"/>}
|
|
text={`${seekingGenderText} ${ageRangeText}`}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function RelationshipType(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const seekingGenderText = getSeekingGenderText(profile, t)
|
|
return (
|
|
<AboutRow
|
|
icon={<BsPersonHeart className="h-5 w-5"/>}
|
|
text={seekingGenderText}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function RelationshipStatus(props: { profile: Profile }) {
|
|
const {profile} = props
|
|
const t = useT()
|
|
const relationship_status = profile.relationship_status ?? []
|
|
if (relationship_status.length === 0) return
|
|
const key = relationship_status[0] as keyof typeof RELATIONSHIP_ICONS
|
|
const icon = RELATIONSHIP_ICONS[key] ?? FaHeart
|
|
return (
|
|
<AboutRow
|
|
icon={icon ? React.createElement(icon, {className: 'h-5 w-5'}) : null}
|
|
text={relationship_status?.map(v => t(`profile.relationship_status.${v}`, INVERTED_RELATIONSHIP_STATUS_CHOICES[v]))}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function Education(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const educationLevel = profile.education_level
|
|
const university = profile.university
|
|
|
|
let text = ''
|
|
|
|
if (educationLevel) {
|
|
text += capitalizeAndRemoveUnderscores(t(`profile.education.${educationLevel}`, INVERTED_EDUCATION_CHOICES[educationLevel]))
|
|
}
|
|
if (university) {
|
|
if (educationLevel) text += ` ${t('profile.at', 'at')} `
|
|
text += capitalizeAndRemoveUnderscores(university)
|
|
}
|
|
if (text.length === 0) {
|
|
return <></>
|
|
}
|
|
return (
|
|
<AboutRow
|
|
icon={<LuGraduationCap className="h-5 w-5"/>}
|
|
text={text}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function Occupation(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const occupation_title = profile.occupation_title
|
|
const company = profile.company
|
|
|
|
if (!company && !occupation_title) {
|
|
return <></>
|
|
}
|
|
const occupationText = `${
|
|
occupation_title ? capitalizeAndRemoveUnderscores(occupation_title) : ''
|
|
}${occupation_title && company ? ` ${t('profile.at', 'at')} ` : ''}${
|
|
company ? capitalizeAndRemoveUnderscores(company) : ''
|
|
}`
|
|
return (
|
|
<AboutRow
|
|
icon={<LuBriefcase className="h-5 w-5"/>}
|
|
text={occupationText}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function Smoker(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const isSmoker = profile.is_smoker
|
|
if (isSmoker == null) return null
|
|
if (isSmoker) {
|
|
return (
|
|
<AboutRow icon={<LuCigarette className="h-5 w-5"/>} text={t('profile.smokes', 'Smokes')}/>
|
|
)
|
|
}
|
|
return (
|
|
<AboutRow
|
|
icon={<LuCigaretteOff className="h-5 w-5"/>}
|
|
text={t('profile.doesnt_smoke', "Doesn't smoke")}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function Drinks(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const drinksPerMonth = profile.drinks_per_month
|
|
if (drinksPerMonth == null) return null
|
|
if (drinksPerMonth === 0) {
|
|
return (
|
|
<AboutRow
|
|
icon={<MdNoDrinks className="h-5 w-5"/>}
|
|
text={t('profile.doesnt_drink', "Doesn't drink")}
|
|
/>
|
|
)
|
|
}
|
|
return (
|
|
<AboutRow
|
|
icon={<BiSolidDrink className="h-5 w-5"/>}
|
|
text={
|
|
drinksPerMonth === 1
|
|
? t('profile.drinks_one', '1 drink per month')
|
|
: t('profile.drinks_many', '{count} drinks per month', {count: drinksPerMonth})
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function WantsKids(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
const wantsKidsStrength = profile.wants_kids_strength
|
|
if (wantsKidsStrength == null || wantsKidsStrength < 0) return null
|
|
const wantsKidsText =
|
|
wantsKidsStrength == 0
|
|
? t('profile.wants_kids_0', "Does not want children")
|
|
: wantsKidsStrength == 1
|
|
? t('profile.wants_kids_1', "Prefers not to have children")
|
|
: wantsKidsStrength == 2
|
|
? t('profile.wants_kids_2', "Neutral or open to having children")
|
|
: wantsKidsStrength == 3
|
|
? t('profile.wants_kids_3', "Leaning towards wanting children")
|
|
: t('profile.wants_kids_4', "Wants children")
|
|
|
|
return (
|
|
<AboutRow
|
|
icon={<MdOutlineChildFriendly className="h-5 w-5"/>}
|
|
text={wantsKidsText}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function LastOnline(props: { lastOnlineTime?: string }) {
|
|
const t = useT()
|
|
const {locale} = useLocale()
|
|
const {lastOnlineTime} = props
|
|
if (!lastOnlineTime) return null
|
|
return (
|
|
<AboutRow
|
|
icon={<ClockIcon className="h-5 w-5"/>}
|
|
text={t('profile.last_online', 'Active {time}', {time: fromNow(lastOnlineTime, true, t, locale)})}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function Big5Traits(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
|
|
const traits = [
|
|
{
|
|
key: 'big5_openness',
|
|
icon: <TbBulb className="h-5 w-5"/>,
|
|
label: t('profile.big5_openness', 'Openness'),
|
|
value: profile.big5_openness
|
|
},
|
|
{
|
|
key: 'big5_conscientiousness',
|
|
icon: <TbCheck className="h-5 w-5"/>,
|
|
label: t('profile.big5_conscientiousness', 'Conscientiousness'),
|
|
value: profile.big5_conscientiousness
|
|
},
|
|
{
|
|
key: 'big5_extraversion',
|
|
icon: <TbUsers className="h-5 w-5"/>,
|
|
label: t('profile.big5_extraversion', 'Extraversion'),
|
|
value: profile.big5_extraversion
|
|
},
|
|
{
|
|
key: 'big5_agreeableness',
|
|
icon: <FaHeart className="h-5 w-5"/>,
|
|
label: t('profile.big5_agreeableness', 'Agreeableness'),
|
|
value: profile.big5_agreeableness
|
|
},
|
|
{
|
|
key: 'big5_neuroticism',
|
|
icon: <TbMoodSad className="h-5 w-5"/>,
|
|
label: t('profile.big5_neuroticism', 'Neuroticism'),
|
|
value: profile.big5_neuroticism
|
|
}
|
|
]
|
|
|
|
const hasAnyTraits = traits.some(trait => trait.value !== null && trait.value !== undefined)
|
|
|
|
if (!hasAnyTraits) {
|
|
return <></>
|
|
}
|
|
|
|
return (
|
|
<Col className="gap-2">
|
|
<div className="text-ink-600 font-medium">
|
|
{t('profile.big5', 'Big Five personality traits')}:
|
|
</div>
|
|
<div className="ml-6">
|
|
{traits.map((trait) => {
|
|
if (trait.value === null || trait.value === undefined) return null
|
|
|
|
let levelText: string
|
|
if (trait.value <= 20) {
|
|
levelText = t('profile.big5_very_low', 'Very low')
|
|
} else if (trait.value <= 40) {
|
|
levelText = t('profile.big5_low', 'Low')
|
|
} else if (trait.value <= 60) {
|
|
levelText = t('profile.big5_average', 'Average')
|
|
} else if (trait.value <= 80) {
|
|
levelText = t('profile.big5_high', 'High')
|
|
} else {
|
|
levelText = t('profile.big5_very_high', 'Very high')
|
|
}
|
|
|
|
return (
|
|
<Row key={trait.key} className="items-center gap-2">
|
|
<div className="text-ink-600 w-5">{trait.icon}</div>
|
|
<div>
|
|
{trait.label}: {levelText} ({trait.value})
|
|
</div>
|
|
</Row>
|
|
)
|
|
})}
|
|
</div>
|
|
</Col>
|
|
)
|
|
}
|
|
|
|
function HasKids(props: { profile: Profile }) {
|
|
const t = useT()
|
|
const {profile} = props
|
|
if (typeof profile.has_kids !== 'number') return null
|
|
const hasKidsText = profile.has_kids == 0
|
|
? t('profile.has_kids.doesnt_have_kids', 'Does not have children')
|
|
: profile.has_kids > 1
|
|
? t('profile.has_kids_many', 'Has {count} kids', {count: profile.has_kids})
|
|
: t('profile.has_kids_one', 'Has {count} kid', {count: profile.has_kids})
|
|
const faChild = <FaChild className="h-5 w-5"/>
|
|
const icon = profile.has_kids === 0 ? (
|
|
<div className="relative h-5 w-5">
|
|
{faChild}
|
|
<div className="absolute inset-0">
|
|
{/*<div className="absolute top-1/2 left-0 h-0.5 w-full -translate-y-1/2 rotate-45 transform bg-ink-500"/>*/}
|
|
<div className="absolute top-1/2 left-0 h-0.5 w-full -translate-y-1/2 -rotate-45 transform bg-ink-1000"/>
|
|
</div>
|
|
</div>
|
|
) : faChild
|
|
return <AboutRow icon={icon} text={hasKidsText}/>
|
|
}
|
|
|
|
export const formatProfileValue = (key: string, value: any) => {
|
|
if (Array.isArray(value)) {
|
|
return value.join(', ')
|
|
}
|
|
switch (key) {
|
|
case 'created_time':
|
|
case 'last_online_time':
|
|
return fromNow(new Date(value).valueOf())
|
|
case 'is_smoker':
|
|
case 'diet':
|
|
case 'has_pets':
|
|
return value ? 'Yes' : 'No'
|
|
case 'height_in_inches':
|
|
return `${Math.floor(value / 12)}' ${Math.round(value % 12)}"`
|
|
case 'pref_age_max':
|
|
case 'pref_age_min':
|
|
return null // handle this in a special case
|
|
case 'wants_kids_strength':
|
|
return renderAgreementScale(value)
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
const renderAgreementScale = (value: number) => {
|
|
if (value == 1) return 'Strongly disagree'
|
|
if (value == 2) return 'Disagree'
|
|
if (value == 3) return 'Neutral'
|
|
if (value == 4) return 'Agree'
|
|
if (value == 5) return 'Strongly agree'
|
|
return ''
|
|
}
|
|
|
|
const capitalizeAndRemoveUnderscores = (str: string) => {
|
|
const withSpaces = str.replace(/_/g, ' ')
|
|
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1)
|
|
}
|