mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-14 01:54:40 -04:00
Add Big 5 profile field
This commit is contained in:
181
web/components/filters/big5-filter.tsx
Normal file
181
web/components/filters/big5-filter.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import clsx from 'clsx'
|
||||
import {RangeSlider} from 'web/components/widgets/slider'
|
||||
import {FilterFields} from 'common/filters'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export const BIG5_MIN = 0
|
||||
export const BIG5_MAX = 100
|
||||
|
||||
export type Big5Key =
|
||||
| 'big5_openness'
|
||||
| 'big5_conscientiousness'
|
||||
| 'big5_extraversion'
|
||||
| 'big5_agreeableness'
|
||||
| 'big5_neuroticism'
|
||||
|
||||
export type Big5MinMaxKey =
|
||||
| 'big5_openness_min'
|
||||
| 'big5_openness_max'
|
||||
| 'big5_conscientiousness_min'
|
||||
| 'big5_conscientiousness_max'
|
||||
| 'big5_extraversion_min'
|
||||
| 'big5_extraversion_max'
|
||||
| 'big5_agreeableness_min'
|
||||
| 'big5_agreeableness_max'
|
||||
| 'big5_neuroticism_min'
|
||||
| 'big5_neuroticism_max'
|
||||
|
||||
export function hasAnyBig5Filter(filters: Partial<FilterFields>) {
|
||||
return (
|
||||
filters.big5_openness_min != null ||
|
||||
filters.big5_openness_max != null ||
|
||||
filters.big5_conscientiousness_min != null ||
|
||||
filters.big5_conscientiousness_max != null ||
|
||||
filters.big5_extraversion_min != null ||
|
||||
filters.big5_extraversion_max != null ||
|
||||
filters.big5_agreeableness_min != null ||
|
||||
filters.big5_agreeableness_max != null ||
|
||||
filters.big5_neuroticism_min != null ||
|
||||
filters.big5_neuroticism_max != null
|
||||
)
|
||||
}
|
||||
|
||||
export function Big5FilterText(props: {
|
||||
filters: Partial<FilterFields>
|
||||
highlightedClass?: string
|
||||
}) {
|
||||
const {filters, highlightedClass} = props
|
||||
const t = useT()
|
||||
const hasAny = hasAnyBig5Filter(filters)
|
||||
|
||||
if (!hasAny) {
|
||||
return (
|
||||
<span className={clsx(!hasAny && 'text-ink-600')}>
|
||||
{t('filter.big5.any', 'Any Big 5')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
{t('filter.big5.custom', 'Custom Big 5')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function Big5SliderRow(props: {
|
||||
label: string
|
||||
minValue: number | null | undefined
|
||||
maxValue: number | null | undefined
|
||||
onChange: (min: number | undefined, max: number | undefined) => void
|
||||
}) {
|
||||
const {label, minValue, maxValue, onChange} = props
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 flex items-center justify-between text-sm text-ink-600">
|
||||
<span>{label}</span>
|
||||
<span className="font-semibold text-ink-700">
|
||||
{minValue == null && maxValue == null
|
||||
? '0 – 100'
|
||||
: `${minValue ?? BIG5_MIN} – ${maxValue ?? BIG5_MAX}`}
|
||||
</span>
|
||||
</div>
|
||||
<RangeSlider
|
||||
lowValue={minValue ?? BIG5_MIN}
|
||||
highValue={maxValue ?? BIG5_MAX}
|
||||
min={BIG5_MIN}
|
||||
max={BIG5_MAX}
|
||||
setValues={(low, high) => {
|
||||
onChange(
|
||||
low > BIG5_MIN ? Math.round(low) : undefined,
|
||||
high < BIG5_MAX ? Math.round(high) : undefined
|
||||
)
|
||||
}}
|
||||
marks={[
|
||||
{value: 0, label: '0'},
|
||||
{value: 25, label: '25'},
|
||||
{value: 50, label: '50'},
|
||||
{value: 75, label: '75'},
|
||||
{value: 100, label: '100'},
|
||||
].map((m) => ({
|
||||
value: ((m.value - BIG5_MIN) / (BIG5_MAX - BIG5_MIN)) * 100,
|
||||
label: m.label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Big5Filters(props: {
|
||||
filters: Partial<FilterFields>
|
||||
updateFilter: (newState: Partial<FilterFields>) => void
|
||||
}) {
|
||||
const {filters, updateFilter} = props
|
||||
const t = useT()
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md py-2">
|
||||
<Big5SliderRow
|
||||
label={t('profile.big5_openness', 'Openness')}
|
||||
minValue={filters.big5_openness_min}
|
||||
maxValue={filters.big5_openness_max}
|
||||
onChange={(min, max) =>
|
||||
updateFilter({
|
||||
big5_openness_min: min,
|
||||
big5_openness_max: max,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Big5SliderRow
|
||||
label={t(
|
||||
'profile.big5_conscientiousness',
|
||||
'Conscientiousness'
|
||||
)}
|
||||
minValue={filters.big5_conscientiousness_min}
|
||||
maxValue={filters.big5_conscientiousness_max}
|
||||
onChange={(min, max) =>
|
||||
updateFilter({
|
||||
big5_conscientiousness_min: min,
|
||||
big5_conscientiousness_max: max,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Big5SliderRow
|
||||
label={t('profile.big5_extraversion', 'Extraversion')}
|
||||
minValue={filters.big5_extraversion_min}
|
||||
maxValue={filters.big5_extraversion_max}
|
||||
onChange={(min, max) =>
|
||||
updateFilter({
|
||||
big5_extraversion_min: min,
|
||||
big5_extraversion_max: max,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Big5SliderRow
|
||||
label={t('profile.big5_agreeableness', 'Agreeableness')}
|
||||
minValue={filters.big5_agreeableness_min}
|
||||
maxValue={filters.big5_agreeableness_max}
|
||||
onChange={(min, max) =>
|
||||
updateFilter({
|
||||
big5_agreeableness_min: min,
|
||||
big5_agreeableness_max: max,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Big5SliderRow
|
||||
label={t('profile.big5_neuroticism', 'Neuroticism')}
|
||||
minValue={filters.big5_neuroticism_min}
|
||||
maxValue={filters.big5_neuroticism_max}
|
||||
onChange={(min, max) =>
|
||||
updateFilter({
|
||||
big5_neuroticism_min: min,
|
||||
big5_neuroticism_max: max,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {InterestFilter, InterestFilterText} from "web/components/filters/interes
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
import {useT} from "web/lib/locale";
|
||||
import {ResetFiltersButton} from "web/components/searches/button";
|
||||
import {Big5Filters, Big5FilterText, hasAnyBig5Filter} from "web/components/filters/big5-filter";
|
||||
|
||||
export function DesktopFilters(props: {
|
||||
filters: Partial<FilterFields>
|
||||
@@ -646,6 +647,31 @@ export function DesktopFilters(props: {
|
||||
menuWidth="w-[400px]"
|
||||
/>
|
||||
|
||||
{/* BIG FIVE PERSONALITY */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open) => (
|
||||
<DropdownButton
|
||||
open={open}
|
||||
content={
|
||||
<Row className="items-center gap-1">
|
||||
<BsPersonVcard className="h-4 w-4"/>
|
||||
<Big5FilterText
|
||||
filters={filters}
|
||||
highlightedClass={open || hasAnyBig5Filter(filters) ? 'text-primary-500' : undefined}
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
dropdownMenuContent={
|
||||
<Col className="mx-2 mb-4">
|
||||
<Big5Filters filters={filters} updateFilter={updateFilter}/>
|
||||
</Col>
|
||||
}
|
||||
popoverClassName="bg-canvas-50"
|
||||
menuWidth="w-[420px]"
|
||||
/>
|
||||
|
||||
{/* EDUCATION */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open: boolean) => (
|
||||
|
||||
@@ -43,6 +43,7 @@ import {LuCigarette, LuGraduationCap} from "react-icons/lu";
|
||||
import {RiScales3Line} from "react-icons/ri";
|
||||
import {PiHandsPrayingBold} from "react-icons/pi";
|
||||
import {ResetFiltersButton} from "web/components/searches/button";
|
||||
import {Big5Filters, Big5FilterText, hasAnyBig5Filter} from "web/components/filters/big5-filter";
|
||||
import {FilterGuide} from "web/components/guidance";
|
||||
|
||||
function MobileFilters(props: {
|
||||
@@ -534,6 +535,25 @@ function MobileFilters(props: {
|
||||
<MbtiFilter filters={filters} updateFilter={updateFilter}/>
|
||||
</MobileFilterSection>
|
||||
|
||||
{/* BIG FIVE PERSONALITY */}
|
||||
<MobileFilterSection
|
||||
title={t('profile.big5', 'Personality (Big Five)')}
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAnyBig5Filter(filters)}
|
||||
icon={<BsPersonVcard className="h-4 w-4"/>}
|
||||
selection={
|
||||
<Big5FilterText
|
||||
filters={filters}
|
||||
highlightedClass={
|
||||
hasAnyBig5Filter(filters) ? 'text-primary-600' : 'text-ink-900'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Big5Filters filters={filters} updateFilter={updateFilter}/>
|
||||
</MobileFilterSection>
|
||||
|
||||
{/* EDUCATION */}
|
||||
<MobileFilterSection
|
||||
title={t("profile.education.short_name", "Education")}
|
||||
|
||||
@@ -47,6 +47,7 @@ import {AddOptionEntry} from "web/components/add-option-entry";
|
||||
import {sleep} from "common/util/time"
|
||||
import {SignupBio} from "web/components/bio/editable-bio";
|
||||
import {Editor} from "@tiptap/core";
|
||||
import {Slider} from "web/components/widgets/slider";
|
||||
|
||||
|
||||
export const OptionalProfileUserForm = (props: {
|
||||
@@ -644,6 +645,55 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/* Big Five personality traits (0–100) */}
|
||||
<Col className={clsx(colClassName, 'max-w-[550px]')}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.big5', 'Big Five Personality Traits')}
|
||||
</label>
|
||||
<div className="space-y-4">
|
||||
<Big5Slider
|
||||
label={t('profile.big5_openness', 'Openness')}
|
||||
value={profile.big5_openness ?? 50}
|
||||
onChange={(v) => setProfile('big5_openness', v)}
|
||||
/>
|
||||
<Big5Slider
|
||||
label={t(
|
||||
'profile.big5_conscientiousness',
|
||||
'Conscientiousness'
|
||||
)}
|
||||
value={profile.big5_conscientiousness ?? 50}
|
||||
onChange={(v) => setProfile('big5_conscientiousness', v)}
|
||||
/>
|
||||
<Big5Slider
|
||||
label={t('profile.big5_extraversion', 'Extraversion')}
|
||||
value={profile.big5_extraversion ?? 50}
|
||||
onChange={(v) => setProfile('big5_extraversion', v)}
|
||||
/>
|
||||
<Big5Slider
|
||||
label={t(
|
||||
'profile.big5_agreeableness',
|
||||
'Agreeableness'
|
||||
)}
|
||||
value={profile.big5_agreeableness ?? 50}
|
||||
onChange={(v) => setProfile('big5_agreeableness', v)}
|
||||
/>
|
||||
<Big5Slider
|
||||
label={t(
|
||||
'profile.big5_neuroticism',
|
||||
'Neuroticism'
|
||||
)}
|
||||
value={profile.big5_neuroticism ?? 50}
|
||||
onChange={(v) => setProfile('big5_neuroticism', v)}
|
||||
/>
|
||||
<p className="text-sm text-ink-500">
|
||||
{t(
|
||||
'profile.big5_hint',
|
||||
'Drag each slider to set where you see yourself on these traits (0 = low, 100 = high).'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.diet', 'Diet')}/>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
@@ -900,3 +950,33 @@ function Category({title, className}: { title: string, className?: string }) {
|
||||
return <h3 className={clsx("text-xl font-semibold mb-[-8px]", className)}>{title}</h3>;
|
||||
}
|
||||
|
||||
const Big5Slider = (props: {
|
||||
label: string
|
||||
value: number
|
||||
onChange: (v: number) => void
|
||||
}) => {
|
||||
const {label, value, onChange} = props
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between text-sm text-ink-600">
|
||||
<span>{label}</span>
|
||||
<span className="font-semibold text-ink-700">{Math.round(value)}</span>
|
||||
</div>
|
||||
<Slider
|
||||
amount={value}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(v) => onChange(Math.round(v))}
|
||||
marks={[
|
||||
{value: 0, label: '0'},
|
||||
{value: 25, label: '25'},
|
||||
{value: 50, label: '50'},
|
||||
{value: 75, label: '75'},
|
||||
{value: 100, label: '100'},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ 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
|
||||
@@ -119,6 +120,7 @@ export default function ProfileAbout(props: {
|
||||
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
|
||||
@@ -340,6 +342,85 @@ function LastOnline(props: { lastOnlineTime?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user