Add Big 5 profile field

This commit is contained in:
MartinBraquet
2026-02-13 15:23:54 +01:00
parent ca55a93d5f
commit 7734b689a3
15 changed files with 576 additions and 11 deletions

View File

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

View File

@@ -17,6 +17,16 @@ export type profileQueryType = {
pref_age_max?: number | undefined,
drinks_min?: number | undefined,
drinks_max?: number | undefined,
big5_openness_min?: number | undefined,
big5_openness_max?: number | undefined,
big5_conscientiousness_min?: number | undefined,
big5_conscientiousness_max?: number | undefined,
big5_extraversion_min?: number | undefined,
big5_extraversion_max?: number | undefined,
big5_agreeableness_min?: number | undefined,
big5_agreeableness_max?: number | undefined,
big5_neuroticism_min?: number | undefined,
big5_neuroticism_max?: number | undefined,
pref_relation_styles?: string[] | undefined,
pref_romantic_styles?: string[] | undefined,
diet?: string[] | undefined,
@@ -60,6 +70,16 @@ export const loadProfiles = async (props: profileQueryType) => {
pref_age_max,
drinks_min,
drinks_max,
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,
pref_relation_styles,
pref_romantic_styles,
diet,
@@ -211,6 +231,42 @@ export const loadProfiles = async (props: profileQueryType) => {
drinks_max &&
where(`drinks_per_month <= $(drinks_max) or drinks_per_month is null`, {drinks_max}),
big5_openness_min &&
where(`big5_openness >= $(big5_openness_min) or big5_openness is null`, {big5_openness_min}),
big5_openness_max &&
where(`big5_openness <= $(big5_openness_max) or big5_openness is null`, {big5_openness_max}),
big5_conscientiousness_min &&
where(
`big5_conscientiousness >= $(big5_conscientiousness_min) or big5_conscientiousness is null`,
{big5_conscientiousness_min}
),
big5_conscientiousness_max &&
where(
`big5_conscientiousness <= $(big5_conscientiousness_max) or big5_conscientiousness is null`,
{big5_conscientiousness_max}
),
big5_extraversion_min &&
where(`big5_extraversion >= $(big5_extraversion_min) or big5_extraversion is null`, {big5_extraversion_min}),
big5_extraversion_max &&
where(`big5_extraversion <= $(big5_extraversion_max) or big5_extraversion is null`, {big5_extraversion_max}),
big5_agreeableness_min &&
where(`big5_agreeableness >= $(big5_agreeableness_min) or big5_agreeableness is null`, {big5_agreeableness_min}),
big5_agreeableness_max &&
where(`big5_agreeableness <= $(big5_agreeableness_max) or big5_agreeableness is null`, {big5_agreeableness_max}),
big5_neuroticism_min &&
where(`big5_neuroticism >= $(big5_neuroticism_min) or big5_neuroticism is null`, {big5_neuroticism_min}),
big5_neuroticism_max &&
where(`big5_neuroticism <= $(big5_neuroticism_max) or big5_neuroticism is null`, {big5_neuroticism_max}),
pref_relation_styles?.length &&
where(
`pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`,

View File

@@ -113,6 +113,11 @@ export const sinclairProfile: ProfileRow = {
search_text: 'the futa in futarchy',
search_tsv: 'the futa in futarchy',
age: 25,
big5_agreeableness: 10,
big5_openness: 10,
big5_conscientiousness: 10,
big5_extraversion: 10,
big5_neuroticism: 10,
}
export const jamesUser: User = {
@@ -226,4 +231,9 @@ export const jamesProfile: ProfileRow = {
search_text: 'the futa in futarchy',
search_tsv: 'the futa in futarchy',
age: 32,
big5_agreeableness: 10,
big5_openness: 10,
big5_conscientiousness: 10,
big5_extraversion: 10,
big5_neuroticism: 10,
}

View File

@@ -0,0 +1,23 @@
ALTER TABLE profiles
ADD COLUMN big5_openness SMALLINT,
ADD COLUMN big5_conscientiousness SMALLINT,
ADD COLUMN big5_extraversion SMALLINT,
ADD COLUMN big5_agreeableness SMALLINT,
ADD COLUMN big5_neuroticism SMALLINT;
ALTER TABLE profiles
ADD CONSTRAINT big5_range_check
CHECK (
big5_openness BETWEEN 0 AND 100 AND
big5_conscientiousness BETWEEN 0 AND 100 AND
big5_extraversion BETWEEN 0 AND 100 AND
big5_agreeableness BETWEEN 0 AND 100 AND
big5_neuroticism BETWEEN 0 AND 100
);
CREATE INDEX profiles_big5_open_idx ON profiles (big5_openness);
CREATE INDEX profiles_big5_cons_idx ON profiles (big5_conscientiousness);
CREATE INDEX profiles_big5_extra_idx ON profiles (big5_extraversion);
CREATE INDEX profiles_big5_agree_idx ON profiles (big5_agreeableness);
CREATE INDEX profiles_big5_neur_idx ON profiles (big5_neuroticism);

View File

@@ -509,6 +509,16 @@ export const API = (_apiTypeCheck = {
pref_age_max: z.coerce.number().optional(),
drinks_min: z.coerce.number().optional(),
drinks_max: z.coerce.number().optional(),
big5_openness_min: z.coerce.number().optional(),
big5_openness_max: z.coerce.number().optional(),
big5_conscientiousness_min: z.coerce.number().optional(),
big5_conscientiousness_max: z.coerce.number().optional(),
big5_extraversion_min: z.coerce.number().optional(),
big5_extraversion_max: z.coerce.number().optional(),
big5_agreeableness_min: z.coerce.number().optional(),
big5_agreeableness_max: z.coerce.number().optional(),
big5_neuroticism_min: z.coerce.number().optional(),
big5_neuroticism_max: z.coerce.number().optional(),
religion: arraybeSchema.optional(),
pref_relation_styles: arraybeSchema.optional(),
pref_romantic_styles: arraybeSchema.optional(),

View File

@@ -56,6 +56,7 @@ export const baseProfilesSchema = z.object({
country: z.string().optional().nullable(),
gender: genderType,
geodb_city_id: z.string().optional().nullable(),
languages: z.array(z.string()).optional().nullable(),
looking_for_matches: zBoolean,
photo_urls: z.array(z.string()).nullable(),
pinned_url: z.string(),
@@ -67,13 +68,18 @@ export const baseProfilesSchema = z.object({
region_code: z.string().optional().nullable(),
visibility: z.union([z.literal('public'), z.literal('member')]),
wants_kids_strength: z.number().nullable(),
languages: z.array(z.string()).optional().nullable(),
})
const optionalProfilesSchema = z.object({
avatar_url: z.string().optional().nullable(),
bio: contentSchema.optional().nullable(),
big5_openness: z.number().min(0).max(100).optional().nullable(),
big5_conscientiousness: z.number().min(0).max(100).optional().nullable(),
big5_extraversion: z.number().min(0).max(100).optional().nullable(),
big5_agreeableness: z.number().min(0).max(100).optional().nullable(),
big5_neuroticism: z.number().min(0).max(100).optional().nullable(),
born_in_location: z.string().optional().nullable(),
causes: z.array(z.string()).optional().nullable(),
comments_enabled: zBoolean.optional(),
company: z.string().optional().nullable(),
diet: z.array(z.string()).optional().nullable(),
@@ -82,29 +88,27 @@ const optionalProfilesSchema = z.object({
drinks_min: z.number().min(0).optional().nullable(),
drinks_per_month: z.number().min(0).optional().nullable(),
education_level: z.string().optional().nullable(),
mbti: z.string().optional().nullable(),
ethnicity: z.array(z.string()).optional().nullable(),
has_kids: z.number().min(0).optional().nullable(),
has_pets: zBoolean.optional().nullable(),
height_in_inches: z.number().optional().nullable(),
image_descriptions: z.any().optional().nullable(),
interests: z.array(z.string()).optional().nullable(),
is_smoker: zBoolean.optional().nullable(),
mbti: z.string().optional().nullable(),
occupation: z.string().optional().nullable(),
occupation_title: z.string().optional().nullable(),
political_beliefs: z.array(z.string()).optional().nullable(),
interests: z.array(z.string()).optional().nullable(),
work: z.array(z.string()).optional().nullable(),
causes: z.array(z.string()).optional().nullable(),
relationship_status: z.array(z.string()).optional().nullable(),
political_details: z.string().optional().nullable(),
pref_romantic_styles: z.array(z.string()).nullable(),
relationship_status: z.array(z.string()).optional().nullable(),
religion: z.array(z.string()).optional().nullable(),
religious_belief_strength: z.number().optional().nullable(),
religious_beliefs: z.string().optional().nullable(),
twitter: z.string().optional().nullable(),
university: z.string().optional().nullable(),
website: z.string().optional().nullable(),
image_descriptions: z.any().optional().nullable(),
work: z.array(z.string()).optional().nullable(),
})
export const combinedProfileSchema =
baseProfilesSchema.merge(optionalProfilesSchema)
export const combinedProfileSchema = baseProfilesSchema.merge(optionalProfilesSchema)

View File

@@ -22,6 +22,17 @@ export type FilterFields = {
shortBio: boolean | undefined
drinks_min: number | undefined
drinks_max: number | undefined
// Big Five personality filters (0-100 range)
big5_openness_min: number | undefined
big5_openness_max: number | undefined
big5_conscientiousness_min: number | undefined
big5_conscientiousness_max: number | undefined
big5_extraversion_min: number | undefined
big5_extraversion_max: number | undefined
big5_agreeableness_min: number | undefined
big5_agreeableness_max: number | undefined
big5_neuroticism_min: number | undefined
big5_neuroticism_max: number | undefined
} & {
[K in OptionTableKey]: string[]
}
@@ -89,6 +100,16 @@ export const initialFilters: Partial<FilterFields> = {
shortBio: undefined,
drinks_min: undefined,
drinks_max: undefined,
big5_openness_min: undefined,
big5_openness_max: undefined,
big5_conscientiousness_min: undefined,
big5_conscientiousness_max: undefined,
big5_extraversion_min: undefined,
big5_extraversion_max: undefined,
big5_agreeableness_min: undefined,
big5_agreeableness_max: undefined,
big5_neuroticism_min: undefined,
big5_neuroticism_max: undefined,
orderBy: 'created_time',
}

View File

@@ -856,6 +856,11 @@ export type Database = {
profiles: {
Row: {
age: number | null
big5_agreeableness: number | null
big5_conscientiousness: number | null
big5_extraversion: number | null
big5_neuroticism: number | null
big5_openness: number | null
bio: Json | null
bio_length: number | null
bio_text: string | null
@@ -913,6 +918,11 @@ export type Database = {
}
Insert: {
age?: number | null
big5_agreeableness?: number | null
big5_conscientiousness?: number | null
big5_extraversion?: number | null
big5_neuroticism?: number | null
big5_openness?: number | null
bio?: Json | null
bio_length?: number | null
bio_text?: string | null
@@ -970,6 +980,11 @@ export type Database = {
}
Update: {
age?: number | null
big5_agreeableness?: number | null
big5_conscientiousness?: number | null
big5_extraversion?: number | null
big5_neuroticism?: number | null
big5_openness?: number | null
bio?: Json | null
bio_length?: number | null
bio_text?: string | null
@@ -1204,7 +1219,15 @@ export type Database = {
ts?: string
user_id?: string | null
}
Relationships: []
Relationships: [
{
foreignKeyName: 'user_events_user_id_fkey'
columns: ['user_id']
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
},
]
}
user_notifications: {
Row: {

View 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>
)
}

View File

@@ -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) => (

View File

@@ -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")}

View File

@@ -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 (0100) */}
<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>
)
}

View File

@@ -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

View File

@@ -212,6 +212,8 @@
"filter.any_relationship": "Jede romantische Beziehung",
"filter.any_religion": "Jede Religion",
"filter.any_work": "Jede Arbeit",
"filter.big5.any": "Beliebige Big 5",
"filter.big5.custom": "Benutzerdefinierte Big 5",
"filter.gender.any": "Alle",
"filter.gender.gender": "Geschlecht",
"filter.gender.genders": "Geschlechter",
@@ -647,6 +649,19 @@
"profile.optional.username_or_url": "Benutzername oder URL",
"profile.optional.want_kids": "Ich möchte Kinder haben",
"profile.optional.work": "Arbeit",
"profile.big5_openness": "Offenheit",
"profile.big5_conscientiousness": "Gewissenhaftigkeit",
"profile.big5_extraversion": "Extraversion",
"profile.big5_agreeableness": "Verträglichkeit",
"profile.big5_neuroticism": "Neurotizismus",
"profile.big5": "Persönlichkeit (Big Five)",
"profile.big5_hint": "Ziehen Sie jeden Schieber, um festzulegen, wo Sie sich bei diesen Merkmalen sehen (0 = niedrig, 100 = hoch).",
"profile.big5_very_low": "Sehr niedrig",
"profile.big5_low": "Niedrig",
"profile.big5_average": "Durchschnittlich",
"profile.big5_high": "Hoch",
"profile.big5_very_high": "Sehr hoch",
"profile.optional.big5_hint": "Ziehen Sie jeden Schieber, um festzulegen, wo Sie sich bei diesen Merkmalen sehen (0 = niedrig, 100 = hoch).",
"profile.political.conservative": "Konservativ",
"profile.political.e/acc": "Effektiver Akzelerationismus",
"profile.political.green": "Grün / Ökosozialismus",

View File

@@ -212,6 +212,8 @@
"filter.any_relationship": "Toute relation amoureuse",
"filter.any_religion": "Toute religion",
"filter.any_work": "Tout travail",
"filter.big5.any": "Tout Big 5",
"filter.big5.custom": "Certains Big 5",
"filter.gender.any": "Tout",
"filter.gender.gender": "genre",
"filter.gender.genders": "genres",
@@ -647,6 +649,19 @@
"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.big5_openness": "Ouverture",
"profile.big5_conscientiousness": "Conscienciosité",
"profile.big5_extraversion": "Extraversion",
"profile.big5_agreeableness": "Agréabilité",
"profile.big5_neuroticism": "Névrosisme",
"profile.big5": "Big Five",
"profile.big5_hint": "Faites glisser chaque curseur pour définir où vous vous situez sur ces traits (0 = faible, 100 = élevé).",
"profile.big5_very_low": "Très faible",
"profile.big5_low": "Faible",
"profile.big5_average": "Moyen",
"profile.big5_high": "Élevé",
"profile.big5_very_high": "Très élevé",
"profile.optional.big5_hint": "Faites glisser chaque curseur pour définir où vous vous situez sur ces traits (0 = faible, 100 = élevé).",
"profile.political.conservative": "Conservateur·trice",
"profile.political.e/acc": "Accélérationnisme efficace",
"profile.political.green": "Vert·e / Éco-socialiste",