mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 09:48:47 -04:00
Add neurotype and accessibility fields to profiles
- Added `accessibility_notes` (free text) to profiles to capture members' practical access needs while avoiding filterable attributes. - Introduced `neurotype` (selectable values with details option) and GIN indexing to enable efficient keyword searches. - Updated UI components, filters, and forms to support these new profile attributes. - Modified backend and tests to handle the changes and ensure consistency across filtering, seeding, and display logic. - Bumped API version to `1.49.0`.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
GENDERS,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
NEUROTYPE_CHOICES,
|
||||
ORIENTATION_CHOICES,
|
||||
POLITICAL_CHOICES,
|
||||
RACE_CHOICES,
|
||||
@@ -81,6 +82,7 @@ export type profileQueryType = {
|
||||
languages?: string[] | undefined
|
||||
religion?: string[] | undefined
|
||||
orientation?: string[] | undefined
|
||||
neurotype?: string[] | undefined
|
||||
wants_kids_strength?: number | undefined
|
||||
has_kids?: number | undefined
|
||||
is_smoker?: boolean | undefined
|
||||
@@ -131,6 +133,9 @@ const textFields = [
|
||||
'religious_beliefs',
|
||||
'orientation_details',
|
||||
'gender_details',
|
||||
'neurotype_details',
|
||||
// Keyword-searchable but deliberately has no filter of its own — see the migration for why.
|
||||
'accessibility_notes',
|
||||
]
|
||||
|
||||
// Define choice fields to search
|
||||
@@ -147,6 +152,7 @@ const arrayChoiceFields = [
|
||||
{field: 'relationship_status', choices: RELATIONSHIP_STATUS_CHOICES},
|
||||
{field: 'religion', choices: RELIGION_CHOICES},
|
||||
{field: 'orientation', choices: ORIENTATION_CHOICES},
|
||||
{field: 'neurotype', choices: NEUROTYPE_CHOICES},
|
||||
{field: 'pref_relation_styles', choices: RELATIONSHIP_CHOICES},
|
||||
{field: 'pref_romantic_styles', choices: ROMANTIC_CHOICES},
|
||||
{field: 'languages', choices: LANGUAGE_CHOICES},
|
||||
@@ -211,6 +217,7 @@ export const loadProfiles = async (props: profileQueryType, db?: SupabaseDirectC
|
||||
languages,
|
||||
religion,
|
||||
orientation,
|
||||
neurotype,
|
||||
wants_kids_strength,
|
||||
has_kids,
|
||||
interests,
|
||||
@@ -508,6 +515,17 @@ export const loadProfiles = async (props: profileQueryType, db?: SupabaseDirectC
|
||||
},
|
||||
),
|
||||
|
||||
neurotype?.length &&
|
||||
where(
|
||||
// most are neurotypical, so we don't include the people who didn't answer if we're looking for
|
||||
// anything else — a rare value is only meaningful when it was actually stated
|
||||
(!neurotype.includes('neurotypical') ? '' : "neurotype IS NULL OR neurotype = '{}' OR ") +
|
||||
`neurotype && $(neurotype)`,
|
||||
{
|
||||
neurotype,
|
||||
},
|
||||
),
|
||||
|
||||
interests?.length && where(getManyToManyClause('interests'), {values: interests.map(Number)}),
|
||||
|
||||
causes?.length && where(getManyToManyClause('causes'), {values: causes.map(Number)}),
|
||||
|
||||
@@ -58,4 +58,6 @@ BEGIN;
|
||||
\i backend/supabase/email_unsubscribe_tokens.sql
|
||||
\i backend/supabase/migrations/20260524_add_orientation_to_profiles.sql
|
||||
\i backend/supabase/migrations/20260709_add_last_checked_at_to_bookmarked_searches.sql
|
||||
\i backend/supabase/migrations/20260724_add_neurotype_to_profiles.sql
|
||||
\i backend/supabase/migrations/20260724_add_accessibility_notes_to_profiles.sql
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Add accessibility notes to profiles table
|
||||
-- Created: 2026-07-24
|
||||
--
|
||||
-- Deliberately free text rather than a checkbox taxonomy of conditions: a taxonomy exists mainly to be
|
||||
-- filtered on, and filtering people out by disability is exactly what this field must not enable. Free
|
||||
-- text keeps framing and depth of disclosure with the member. It is keyword-searchable (see `textFields`
|
||||
-- in backend/api/src/get-profiles.ts) but has no filter of its own.
|
||||
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS accessibility_notes TEXT;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add neurotype (neurodivergence identity) fields to profiles table
|
||||
-- Created: 2026-07-24
|
||||
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS neurotype TEXT[],
|
||||
ADD COLUMN IF NOT EXISTS neurotype_details TEXT;
|
||||
|
||||
-- Create GIN index for array field
|
||||
CREATE INDEX IF NOT EXISTS profiles_neurotype_gin ON profiles USING GIN (neurotype);
|
||||
@@ -387,6 +387,7 @@
|
||||
"filter.any_interests": "Jedes Interesse",
|
||||
"filter.any_language": "Alle Sprachen",
|
||||
"filter.any_mbti": "Jeder MBTI",
|
||||
"filter.any_neurotype": "Beliebiger Neurotyp",
|
||||
"filter.any_new_users": "Jegliche neue Nutzer",
|
||||
"filter.any_orientation": "Jede Orientierung",
|
||||
"filter.any_politics": "Jede politische Ausrichtung",
|
||||
@@ -451,6 +452,7 @@
|
||||
"filter.mine_toggle": "Meine Filter",
|
||||
"filter.multiple": "Mehrere",
|
||||
"filter.near": "in der Nähe von",
|
||||
"filter.neurotype.show_more": "Mehr Neurotypen anzeigen",
|
||||
"filter.orientation.show_more": "Mehr Orientierungen anzeigen",
|
||||
"filter.raised_in": "Aufgewachsen",
|
||||
"filter.relationship.any_connection": "Jede Beziehung",
|
||||
@@ -752,6 +754,7 @@
|
||||
"privacy.use.text": "Ihre Daten werden nur verwendet, um die Plattform zu betreiben, zu personalisieren und zu verbessern. Wir verkaufen Ihre persönlichen Informationen nicht an Dritte.",
|
||||
"privacy.use.title": "2. Wie wir Ihre Daten verwenden",
|
||||
"profile.about.raised_in": "Aufgewachsen in {location}",
|
||||
"profile.accessibility_notes": "Barrierefreiheit",
|
||||
"profile.age_any": "jeden Alters",
|
||||
"profile.age_between": "zwischen {min} und {max} Jahren",
|
||||
"profile.age_exact": "genau {min} Jahre",
|
||||
@@ -1057,7 +1060,19 @@
|
||||
"profile.mbti.ESFP": "Unterhalter",
|
||||
"profile.mutual": "Gegenseitig",
|
||||
"profile.mutual_interests": "Sie haben beide gemeinsame Interessen: {matches}",
|
||||
"profile.neurotype": "Neurotyp",
|
||||
"profile.neurotype.adhd": "ADHS",
|
||||
"profile.neurotype.autistic": "Autistisch",
|
||||
"profile.neurotype.details_placeholder": "Was hilfreich zu wissen ist über dein Denken oder Kommunizieren…",
|
||||
"profile.neurotype.dyscalculic": "Dyskalkulie",
|
||||
"profile.neurotype.dyslexic": "Legasthenie",
|
||||
"profile.neurotype.hsp": "Hochsensibel (HSP)",
|
||||
"profile.neurotype.neurotypical": "Neurotypisch",
|
||||
"profile.neurotype.show_more": "Weitere Optionen anzeigen",
|
||||
"profile.not_both_open_to_type": "Sie sind nicht beide für eine(n) {type} offen",
|
||||
"profile.optional.accessibility_notes": "Barrierefreiheit",
|
||||
"profile.optional.accessibility_notes_hint": "Optional und öffentlich wie der Rest deines Profils. Alles Praktische, das ein gutes Treffen erleichtert — Zugangsbedürfnisse, Energielevel, sensorische Vorlieben, ruhigere Orte. Sag so viel oder so wenig, wie du möchtest.",
|
||||
"profile.optional.accessibility_notes_placeholder": "z. B. Ich nutze einen Rollstuhl, stufenlose Orte passen am besten. Fragen sind willkommen.",
|
||||
"profile.optional.age": "Alter",
|
||||
"profile.optional.age.error_max": "Bitte gib ein gültiges Alter ein",
|
||||
"profile.optional.age.error_min": "Du musst mindestens 18 Jahre alt sein",
|
||||
@@ -1101,6 +1116,7 @@
|
||||
"profile.optional.languages": "Gesprochene Sprachen",
|
||||
"profile.optional.location": "Standort",
|
||||
"profile.optional.mbti": "MBTI-Persönlichkeitstyp",
|
||||
"profile.optional.neurotype": "Neurotyp",
|
||||
"profile.optional.num_kids": "Aktuelle Anzahl von Kindern",
|
||||
"profile.optional.og_card": "Profilkarte",
|
||||
"profile.optional.orientation": "Sexuelle Orientierung",
|
||||
|
||||
@@ -387,6 +387,7 @@
|
||||
"filter.any_interests": "Tout intérêt",
|
||||
"filter.any_language": "Toutes les langues",
|
||||
"filter.any_mbti": "Tout MBTI",
|
||||
"filter.any_neurotype": "Tout neurotype",
|
||||
"filter.any_new_users": "Tout nouvel utilisateur",
|
||||
"filter.any_orientation": "Toute orientation",
|
||||
"filter.any_politics": "Toute orientation politique",
|
||||
@@ -451,6 +452,7 @@
|
||||
"filter.mine_toggle": "Mes filtres",
|
||||
"filter.multiple": "Multiple",
|
||||
"filter.near": "près de",
|
||||
"filter.neurotype.show_more": "Afficher plus de neurotypes",
|
||||
"filter.orientation.show_more": "Afficher plus d'orientations",
|
||||
"filter.raised_in": "A grandi",
|
||||
"filter.relationship.any_connection": "Toute relation",
|
||||
@@ -751,6 +753,7 @@
|
||||
"privacy.use.text": "Vos données sont utilisées uniquement pour faire fonctionner, personnaliser et améliorer la plateforme. Nous ne vendons pas vos informations personnelles à des tiers.",
|
||||
"privacy.use.title": "2. Comment nous utilisons vos données",
|
||||
"profile.about.raised_in": "A grandi à {location}",
|
||||
"profile.accessibility_notes": "Accessibilité",
|
||||
"profile.age_any": "de tout âge",
|
||||
"profile.age_between": "entre {min} et {max} ans",
|
||||
"profile.age_exact": "exactement {min} ans",
|
||||
@@ -1056,7 +1059,19 @@
|
||||
"profile.mbti.ESFP": "Animateur",
|
||||
"profile.mutual": "Mutuel",
|
||||
"profile.mutual_interests": "Vous avez tous les deux des intérêts communs : {matches}. Contactez-les ci-dessus!",
|
||||
"profile.neurotype": "Neurotype",
|
||||
"profile.neurotype.adhd": "TDAH",
|
||||
"profile.neurotype.autistic": "Autiste",
|
||||
"profile.neurotype.details_placeholder": "Ce qu'il est utile de savoir sur votre façon de penser ou de communiquer…",
|
||||
"profile.neurotype.dyscalculic": "Dyscalculique",
|
||||
"profile.neurotype.dyslexic": "Dyslexique",
|
||||
"profile.neurotype.hsp": "Hypersensible (HSP)",
|
||||
"profile.neurotype.neurotypical": "Neurotypique",
|
||||
"profile.neurotype.show_more": "Afficher plus d'options",
|
||||
"profile.not_both_open_to_type": "Vous n'êtes pas tous les deux ouverts à un(e) {type}",
|
||||
"profile.optional.accessibility_notes": "Accessibilité",
|
||||
"profile.optional.accessibility_notes_hint": "Facultatif, et public comme le reste de votre profil. Tout ce qui est utile en pratique pour bien vous rencontrer — besoins d'accès, niveau d'énergie, préférences sensorielles, lieux plus calmes. Dites-en autant ou aussi peu que vous voulez.",
|
||||
"profile.optional.accessibility_notes_placeholder": "ex. Je me déplace en fauteuil roulant, les lieux sans marches sont donc plus simples. N'hésitez pas à poser des questions.",
|
||||
"profile.optional.age": "Âge",
|
||||
"profile.optional.age.error_max": "Veuillez entrer un âge valide",
|
||||
"profile.optional.age.error_min": "Vous devez avoir au moins 18 ans",
|
||||
@@ -1100,6 +1115,7 @@
|
||||
"profile.optional.languages": "Langues parlées",
|
||||
"profile.optional.location": "Localisation",
|
||||
"profile.optional.mbti": "Type de personnalité MBTI",
|
||||
"profile.optional.neurotype": "Neurotype",
|
||||
"profile.optional.num_kids": "Nombre actuel d'enfants",
|
||||
"profile.optional.og_card": "Carte de profil",
|
||||
"profile.optional.orientation": "Orientation sexuelle",
|
||||
|
||||
@@ -762,6 +762,7 @@ export const API = (_apiTypeCheck = {
|
||||
big5_neuroticism_max: z.coerce.number().optional(),
|
||||
religion: arraybeSchema.optional(),
|
||||
orientation: arraybeSchema.optional(),
|
||||
neurotype: arraybeSchema.optional(),
|
||||
pref_relation_styles: arraybeSchema.optional(),
|
||||
pref_romantic_styles: arraybeSchema.optional(),
|
||||
diet: arraybeSchema.optional(),
|
||||
|
||||
@@ -123,6 +123,9 @@ const optionalProfilesSchema = z.object({
|
||||
gender_details: z.string().optional().nullable(),
|
||||
orientation: z.array(z.string()).optional().nullable(),
|
||||
orientation_details: z.string().optional().nullable(),
|
||||
neurotype: z.array(z.string()).optional().nullable(),
|
||||
neurotype_details: z.string().optional().nullable(),
|
||||
accessibility_notes: z.string().optional().nullable(),
|
||||
religion: z.array(z.string()).optional().nullable(),
|
||||
religious_belief_strength: z.number().optional().nullable(),
|
||||
religious_beliefs: z.string().optional().nullable(),
|
||||
|
||||
@@ -314,6 +314,27 @@ export const EXTRA_ORIENTATIONS = Object.values(ORIENTATION_CHOICES).filter(
|
||||
(v) => !DEFAULT_ORIENTATIONS.includes(v as any),
|
||||
)
|
||||
|
||||
// Neurotype is framed as an identity, not a diagnosis: no "undiagnosed" or "prefer not to say" options,
|
||||
// and no psychiatric conditions. Anything a member doesn't want structured goes in `neurotype_details`.
|
||||
export const NEUROTYPE_CHOICES = {
|
||||
Autistic: 'autistic',
|
||||
ADHD: 'adhd',
|
||||
Neurotypical: 'neurotypical',
|
||||
// No combined "AuDHD" option on purpose — that is ticking Autistic and ADHD together.
|
||||
Dyslexic: 'dyslexic',
|
||||
Dyscalculic: 'dyscalculic',
|
||||
'Highly sensitive (HSP)': 'hsp',
|
||||
} as const
|
||||
|
||||
export const DEFAULT_NEUROTYPES = [
|
||||
NEUROTYPE_CHOICES.Autistic,
|
||||
NEUROTYPE_CHOICES.ADHD,
|
||||
NEUROTYPE_CHOICES.Neurotypical,
|
||||
]
|
||||
export const EXTRA_NEUROTYPES = Object.values(NEUROTYPE_CHOICES).filter(
|
||||
(v) => !DEFAULT_NEUROTYPES.includes(v as any),
|
||||
)
|
||||
|
||||
export const LAST_ONLINE_CHOICES = {
|
||||
now: 'Currently online',
|
||||
today: 'Today',
|
||||
@@ -375,6 +396,7 @@ export const INVERTED_CANNABIS_CHOICES = invert(CANNABIS_CHOICES)
|
||||
export const INVERTED_SUBSTANCE_INTENTION_CHOICES = invert(SUBSTANCE_INTENTION_CHOICES)
|
||||
export const INVERTED_SUBSTANCE_PREFERENCE_CHOICES = invert(SUBSTANCE_PREFERENCE_CHOICES)
|
||||
export const INVERTED_ORIENTATION_CHOICES = invert(ORIENTATION_CHOICES)
|
||||
export const INVERTED_NEUROTYPE_CHOICES = invert(NEUROTYPE_CHOICES)
|
||||
|
||||
//Exported types for test files to use when referencing the keys of the choices objects
|
||||
export type PersonalityKey = keyof typeof MBTI_CHOICES
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
INVERTED_GENDERS,
|
||||
INVERTED_LANGUAGE_CHOICES,
|
||||
INVERTED_MBTI_CHOICES,
|
||||
INVERTED_NEUROTYPE_CHOICES,
|
||||
INVERTED_ORIENTATION_CHOICES,
|
||||
INVERTED_POLITICAL_CHOICES,
|
||||
INVERTED_PSYCHEDELICS_CHOICES,
|
||||
@@ -41,6 +42,7 @@ const filterLabels: Record<string, string> = {
|
||||
work: '',
|
||||
religion: '',
|
||||
orientation: '',
|
||||
neurotype: '',
|
||||
orderBy: '',
|
||||
hasPhoto: '',
|
||||
diet: 'Diet',
|
||||
@@ -186,6 +188,10 @@ export function formatFilters(
|
||||
value = value.map((s) =>
|
||||
translate(`profile.orientation.${s}`, INVERTED_ORIENTATION_CHOICES[s] ?? s),
|
||||
)
|
||||
} else if (key === 'neurotype') {
|
||||
value = value.map((s) =>
|
||||
translate(`profile.neurotype.${s}`, INVERTED_NEUROTYPE_CHOICES[s] ?? s),
|
||||
)
|
||||
} else if (key === 'languages') {
|
||||
value = value.map((s) => translate(`profile.language.${s}`, INVERTED_LANGUAGE_CHOICES[s]))
|
||||
} else if (key === 'pref_gender') {
|
||||
|
||||
@@ -56,6 +56,7 @@ export type FilterFields = {
|
||||
| 'pref_age_max'
|
||||
| 'religion'
|
||||
| 'orientation'
|
||||
| 'neurotype'
|
||||
>
|
||||
|
||||
export const orderProfiles = (profiles: Profile[], starredUserIds: string[] | undefined) => {
|
||||
@@ -107,6 +108,7 @@ export const initialFilters: Partial<FilterFields> = {
|
||||
languages: undefined,
|
||||
religion: undefined,
|
||||
orientation: undefined,
|
||||
neurotype: undefined,
|
||||
mbti: undefined,
|
||||
pref_gender: undefined,
|
||||
shortBio: undefined,
|
||||
|
||||
@@ -1114,6 +1114,7 @@ export type Database = {
|
||||
}
|
||||
profiles: {
|
||||
Row: {
|
||||
accessibility_notes: string | null
|
||||
age: number | null
|
||||
allow_direct_messaging: boolean | null
|
||||
allow_interest_indicating: boolean | null
|
||||
@@ -1158,6 +1159,8 @@ export type Database = {
|
||||
looking_for_matches: boolean
|
||||
mbti: string | null
|
||||
messaging_status: string
|
||||
neurotype: string[] | null
|
||||
neurotype_details: string | null
|
||||
occupation: string | null
|
||||
occupation_title: string | null
|
||||
orientation: string[] | null
|
||||
@@ -1195,6 +1198,7 @@ export type Database = {
|
||||
wants_kids_strength: number | null
|
||||
}
|
||||
Insert: {
|
||||
accessibility_notes?: string | null
|
||||
age?: number | null
|
||||
allow_direct_messaging?: boolean | null
|
||||
allow_interest_indicating?: boolean | null
|
||||
@@ -1239,6 +1243,8 @@ export type Database = {
|
||||
looking_for_matches?: boolean
|
||||
mbti?: string | null
|
||||
messaging_status?: string
|
||||
neurotype?: string[] | null
|
||||
neurotype_details?: string | null
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
orientation?: string[] | null
|
||||
@@ -1276,6 +1282,7 @@ export type Database = {
|
||||
wants_kids_strength?: number | null
|
||||
}
|
||||
Update: {
|
||||
accessibility_notes?: string | null
|
||||
age?: number | null
|
||||
allow_direct_messaging?: boolean | null
|
||||
allow_interest_indicating?: boolean | null
|
||||
@@ -1320,6 +1327,8 @@ export type Database = {
|
||||
looking_for_matches?: boolean
|
||||
mbti?: string | null
|
||||
messaging_status?: string
|
||||
neurotype?: string[] | null
|
||||
neurotype_details?: string | null
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
orientation?: string[] | null
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
- Languages — by languages spoken.
|
||||
- Politics — political beliefs or ideological alignment.
|
||||
- Religion — religious belief or affiliation.
|
||||
- Neurotype — neurodivergence identity (multi-select with extended list hidden behind "Show more").
|
||||
- MBTI — Myers–Briggs personality type.
|
||||
- Big Five — ranges of the Big Five personality traits.
|
||||
- Last Active — how recently they were active on the platform.
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
GENDERS,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
NEUROTYPE_CHOICES,
|
||||
ORIENTATION_CHOICES,
|
||||
POLITICAL_CHOICES,
|
||||
PSYCHEDELICS_CHOICES,
|
||||
@@ -37,6 +38,14 @@ class UserAccountInformationForSeeding {
|
||||
faker.number.int({min: 0, max: Object.values(ORIENTATION_CHOICES).length - 1})
|
||||
],
|
||||
)
|
||||
neurotype = Array.from(
|
||||
{length: faker.number.int({min: 1, max: 2})},
|
||||
() =>
|
||||
Object.values(NEUROTYPE_CHOICES)[
|
||||
faker.number.int({min: 0, max: Object.values(NEUROTYPE_CHOICES).length - 1})
|
||||
],
|
||||
)
|
||||
accessibility_notes = faker.lorem.sentence()
|
||||
pref_age = {
|
||||
min: faker.number.int({min: 18, max: 27}),
|
||||
max: faker.number.int({min: 36, max: 68}),
|
||||
|
||||
@@ -154,6 +154,7 @@ async function seedShowcaseProfile(profile: ShowcaseProfile, userId: string) {
|
||||
age: profile.age,
|
||||
gender: profile.gender,
|
||||
orientation: profile.orientation,
|
||||
neurotype: profile.neurotype ?? null,
|
||||
headline: profile.headline,
|
||||
bio,
|
||||
bio_length: bioText.length,
|
||||
|
||||
@@ -69,6 +69,7 @@ export async function seedDbUser(
|
||||
age: userInfo.age,
|
||||
gender: userInfo.randomElement(userInfo.gender),
|
||||
orientation: userInfo.orientation,
|
||||
neurotype: userInfo.neurotype,
|
||||
headline: userInfo.headline,
|
||||
keywords: profileKeywords,
|
||||
ethnicity: [userInfo.randomElement(userInfo.ethnicity)],
|
||||
@@ -96,6 +97,8 @@ export async function seedDbUser(
|
||||
education_level: userInfo.randomElement(userInfo.education_level),
|
||||
languages: languagesKnown,
|
||||
orientation_details: 'Some orientation details',
|
||||
neurotype_details: 'Some neurotype details',
|
||||
accessibility_notes: userInfo.accessibility_notes,
|
||||
pinned_url: '/images/default-avatar.png',
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface ShowcaseProfile {
|
||||
age: number
|
||||
gender: string
|
||||
orientation: string[]
|
||||
/** Optional — only some showcase profiles state a neurotype, as in real life. */
|
||||
neurotype?: string[]
|
||||
headline: string
|
||||
/** Paragraphs of the bio, in order. */
|
||||
bio: string[]
|
||||
@@ -256,6 +258,7 @@ export const SHOWCASE_PROFILES: ShowcaseProfile[] = [
|
||||
age: 26,
|
||||
gender: 'female',
|
||||
orientation: ['queer'],
|
||||
neurotype: ['autistic'],
|
||||
headline: 'Climate modeller. Meditation twice a day. Will talk about soil for hours.',
|
||||
bio: [
|
||||
'I model regional climate for a living, which means I spend most days being carefully precise about uncertainty and most evenings being asked whether we are doomed. (Short answer: the question is badly posed.)',
|
||||
@@ -310,6 +313,7 @@ export const SHOWCASE_PROFILES: ShowcaseProfile[] = [
|
||||
age: 61,
|
||||
gender: 'male',
|
||||
orientation: ['straight'],
|
||||
neurotype: ['neurotypical'],
|
||||
headline: 'Retired GP, second act as a choir conductor, still learning Bach',
|
||||
bio: [
|
||||
'Forty years of general practice taught me that most of what people need is someone who will actually listen for more than four minutes. I retired two years ago and I am still adjusting to having time.',
|
||||
@@ -418,6 +422,7 @@ export const SHOWCASE_PROFILES: ShowcaseProfile[] = [
|
||||
age: 29,
|
||||
gender: 'non-binary',
|
||||
orientation: ['pansexual'],
|
||||
neurotype: ['adhd', 'hsp'],
|
||||
headline: 'Game designer, amateur mycologist, host of a very small dinner party',
|
||||
bio: [
|
||||
'I design systems for a mid-size indie studio — mostly economies and progression, the invisible plumbing that makes a game feel fair. Before that I studied economics, which turns out to be the same job with worse graphics.',
|
||||
@@ -641,6 +646,7 @@ export const SHOWCASE_PROFILES: ShowcaseProfile[] = [
|
||||
age: 36,
|
||||
gender: 'male',
|
||||
orientation: ['straight', 'demisexual'],
|
||||
neurotype: ['autistic', 'adhd'],
|
||||
headline: 'Open-source maintainer looking for collaborators, not users',
|
||||
bio: [
|
||||
'I maintain a mid-sized open-source database tool that about four thousand companies depend on and roughly nine people contribute to. The maths of that is the most interesting problem in my life right now.',
|
||||
|
||||
@@ -24,6 +24,7 @@ import {IncompleteProfilesToggle} from 'web/components/filters/incomplete-profil
|
||||
import {InterestFilter, InterestFilterText} from 'web/components/filters/interest-filter'
|
||||
import {LanguageFilter, LanguageFilterText} from 'web/components/filters/language-filter'
|
||||
import {MbtiFilter, MbtiFilterText} from 'web/components/filters/mbti-filter'
|
||||
import {NeurotypeFilter, NeurotypeFilterText} from 'web/components/filters/neurotype-filter'
|
||||
import {OrientationFilter, OrientationFilterText} from 'web/components/filters/orientation-filter'
|
||||
import {PoliticalFilter, PoliticalFilterText} from 'web/components/filters/political-filter'
|
||||
import {
|
||||
@@ -727,6 +728,25 @@ function Filters(props: {
|
||||
setOpenGroup={setOpenGroup}
|
||||
// icon={<BsPersonVcard className="h-4 w-4" />}
|
||||
>
|
||||
{/* Grouped with MBTI/Big Five rather than under Relationship — it describes how someone's mind
|
||||
works, and matters just as much for friendship and collaboration as for dating. */}
|
||||
<FilterSection
|
||||
title={t('profile.optional.neurotype', 'Neurotype')}
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny((filters as any).neurotype || undefined)}
|
||||
selection={
|
||||
<NeurotypeFilterText options={(filters as any).neurotype as string[] | undefined} />
|
||||
}
|
||||
>
|
||||
<NeurotypeFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
profile={youProfile}
|
||||
className={''}
|
||||
/>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection
|
||||
title={t('profile.optional.mbti', 'MBTI')}
|
||||
openFilter={openFilter}
|
||||
|
||||
102
web/components/filters/neurotype-filter.tsx
Normal file
102
web/components/filters/neurotype-filter.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
DEFAULT_NEUROTYPES,
|
||||
EXTRA_NEUROTYPES,
|
||||
INVERTED_NEUROTYPE_CHOICES,
|
||||
NEUROTYPE_CHOICES,
|
||||
} from 'common/choices'
|
||||
import {FilterFields} from 'common/filters'
|
||||
import {toKey} from 'common/parsing'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {useState} from 'react'
|
||||
import {MultiCheckbox} from 'web/components/multi-checkbox'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
|
||||
|
||||
export function NeurotypeFilterText(props: {
|
||||
options: string[] | undefined
|
||||
highlightedClass?: string
|
||||
}) {
|
||||
const {options, highlightedClass} = props
|
||||
const length = (options ?? []).length
|
||||
|
||||
const t = useT()
|
||||
|
||||
if (!options || length < 1) {
|
||||
return (
|
||||
<span className={clsx('text-semibold', highlightedClass)}>
|
||||
{t('filter.any_neurotype', 'Any neurotype')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (length > 2) {
|
||||
return (
|
||||
<span>
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
{t('filter.multiple', 'Multiple')}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const convertedTypes = options.map((o) =>
|
||||
t(`profile.neurotype.${toKey(o)}`, INVERTED_NEUROTYPE_CHOICES[o] ?? o),
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
{stringOrStringArrayToText({
|
||||
text: convertedTypes,
|
||||
capitalizeFirstLetterOption: true,
|
||||
t: t,
|
||||
})}{' '}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NeurotypeFilter(props: {
|
||||
filters: Partial<FilterFields>
|
||||
updateFilter: (newState: Partial<FilterFields>) => void
|
||||
className?: string
|
||||
profile?: Profile | null
|
||||
}) {
|
||||
const {filters, updateFilter, className, profile} = props
|
||||
const t = useT()
|
||||
const selected = ((filters as any).neurotype as string[] | undefined) ?? []
|
||||
const hasExtendedSelected =
|
||||
selected.some((v) => EXTRA_NEUROTYPES.includes(v as any)) ||
|
||||
(profile?.neurotype && profile?.neurotype.some((v) => EXTRA_NEUROTYPES.includes(v as any)))
|
||||
const [showAll, setShowAll] = useState(hasExtendedSelected)
|
||||
|
||||
const visibleChoices = Object.fromEntries(
|
||||
Object.entries(NEUROTYPE_CHOICES).filter(
|
||||
([, v]) => showAll || DEFAULT_NEUROTYPES.includes(v as any) || selected.includes(v),
|
||||
),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiCheckbox
|
||||
selected={selected}
|
||||
choices={visibleChoices}
|
||||
translationPrefix={'profile.neurotype'}
|
||||
onChange={(c) => {
|
||||
updateFilter({neurotype: c} as any)
|
||||
}}
|
||||
optionsClassName={className}
|
||||
/>
|
||||
{!showAll && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-600 mt-1 text-sm"
|
||||
onClick={() => setShowAll(true)}
|
||||
>
|
||||
{t('filter.neurotype.show_more', 'Show more neurotypes')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,13 +4,16 @@ import {Editor} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
CANNABIS_CHOICES,
|
||||
DEFAULT_NEUROTYPES,
|
||||
DEFAULT_ORIENTATIONS,
|
||||
DIET_CHOICES,
|
||||
EDUCATION_CHOICES,
|
||||
EXTRA_NEUROTYPES,
|
||||
GENDERS,
|
||||
GENDERS_PLURAL,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
NEUROTYPE_CHOICES,
|
||||
ORIENTATION_CHOICES,
|
||||
POLITICAL_CHOICES,
|
||||
PSYCHEDELICS_CHOICES,
|
||||
@@ -123,6 +126,11 @@ export const OptionalProfileUserForm = (props: {
|
||||
() => !!profile.gender && EXTRA_GENDERS.includes(profile.gender as any),
|
||||
)
|
||||
const [showAllOrientations, setShowAllOrientations] = useState(false)
|
||||
const [showAllNeurotypes, setShowAllNeurotypes] = useState(
|
||||
() =>
|
||||
!!(profile as any).neurotype &&
|
||||
((profile as any).neurotype as string[]).some((v) => EXTRA_NEUROTYPES.includes(v as any)),
|
||||
)
|
||||
const t = useT()
|
||||
const {locale} = useLocale()
|
||||
|
||||
@@ -1049,6 +1057,56 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.category.psychology', 'Psychology')} />
|
||||
|
||||
{/* Sits with MBTI/Big Five rather than under Relationships: it describes how someone's mind
|
||||
works, and it is just as relevant to friendship and collaboration as to dating. */}
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.neurotype', 'Neurotype')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={
|
||||
Object.fromEntries(
|
||||
Object.entries(NEUROTYPE_CHOICES).filter(
|
||||
([, v]) =>
|
||||
showAllNeurotypes ||
|
||||
DEFAULT_NEUROTYPES.includes(v as any) ||
|
||||
((profile as any)['neurotype'] ?? []).includes(v),
|
||||
),
|
||||
) as any
|
||||
}
|
||||
selected={(profile as any)['neurotype'] ?? []}
|
||||
translationPrefix={'profile.neurotype'}
|
||||
onChange={(selected) => setProfile('neurotype' as any, selected)}
|
||||
/>
|
||||
{!showAllNeurotypes && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 mt-1 text-sm font-medium hover:underline"
|
||||
onClick={() => setShowAllNeurotypes(true)}
|
||||
>
|
||||
{t('profile.neurotype.show_more', 'Show more options')}
|
||||
</button>
|
||||
)}
|
||||
{showAllNeurotypes && (
|
||||
<>
|
||||
<p className="mt-1">{t('profile.optional.details', 'Details')}</p>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setProfile('neurotype_details' as any, e.target.value)
|
||||
}
|
||||
className={'w-full sm:w-[700px]'}
|
||||
value={(profile as any)['neurotype_details'] ?? undefined}
|
||||
placeholder={t(
|
||||
'profile.neurotype.details_placeholder',
|
||||
'Anything helpful to know about how you think or communicate…',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName, 'max-w-[550px]')}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.mbti', 'MBTI Personality Type')}
|
||||
@@ -1144,6 +1202,33 @@ export const OptionalProfileUserForm = (props: {
|
||||
</p>
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.accessibility_notes', 'Accessibility')} />
|
||||
|
||||
{/* Free text on purpose, not a checkbox list of conditions: a taxonomy exists mainly to be
|
||||
filtered on, and filtering people out by disability is what this field must not enable.
|
||||
Free text leaves the framing and the depth of disclosure with the member. */}
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx('guidance')}>
|
||||
{t(
|
||||
'profile.optional.accessibility_notes_hint',
|
||||
'Optional, and public like the rest of your profile. Anything practical that helps someone meet you well — access needs, energy levels, sensory preferences, quieter venues.',
|
||||
)}
|
||||
</label>
|
||||
<Textarea
|
||||
data-testid="accessibility_notes"
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('accessibility_notes' as any, e.target.value)
|
||||
}
|
||||
className={'w-full md:w-[700px] bg-canvas-50 border rounded-md p-2'}
|
||||
value={(profile as any)['accessibility_notes'] ?? undefined}
|
||||
maxLength={500}
|
||||
placeholder={t(
|
||||
'profile.optional.accessibility_notes_placeholder',
|
||||
'e.g. I use a wheelchair, so step-free venues work best. Happy to answer questions.',
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.diet', 'Diet')} />
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
INVERTED_EDUCATION_CHOICES,
|
||||
INVERTED_LANGUAGE_CHOICES,
|
||||
INVERTED_MBTI_CHOICES,
|
||||
INVERTED_NEUROTYPE_CHOICES,
|
||||
INVERTED_ORIENTATION_CHOICES,
|
||||
INVERTED_POLITICAL_CHOICES,
|
||||
INVERTED_PSYCHEDELICS_CHOICES,
|
||||
@@ -23,7 +24,7 @@ import {formatHeight, MeasurementSystem} from 'common/measurement-utils'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {Socials} from 'common/socials'
|
||||
import {UserActivity} from 'common/user'
|
||||
import {Home, Languages, Leaf, Salad} from 'lucide-react'
|
||||
import {Accessibility, Brain, Home, Languages, Leaf, Salad} from 'lucide-react'
|
||||
import React, {ReactNode} from 'react'
|
||||
import {BiSolidDrink} from 'react-icons/bi'
|
||||
import {FaHeart} from 'react-icons/fa'
|
||||
@@ -416,6 +417,29 @@ function Orientation(props: {profile: Profile}) {
|
||||
)
|
||||
}
|
||||
|
||||
function Neurotype(props: {profile: Profile}) {
|
||||
const t = useT()
|
||||
const {profile} = props
|
||||
const p = profile as any
|
||||
const neurotype = p.neurotype as string[] | null | undefined
|
||||
const neurotypeDetails = p.neurotype_details as string | null | undefined
|
||||
|
||||
if (!neurotype?.length && !neurotypeDetails) return null
|
||||
|
||||
const neurotypeText = neurotype?.length
|
||||
? formatChoiceList(neurotype, 'neurotype', INVERTED_NEUROTYPE_CHOICES, t)
|
||||
: null
|
||||
|
||||
return (
|
||||
<AboutRow
|
||||
icon={<Brain className="h-5 w-5" />}
|
||||
title={t('profile.neurotype', 'Neurotype')}
|
||||
text={neurotypeText}
|
||||
details={neurotypeDetails ? `"${neurotypeDetails}"` : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Ethnicity(props: {profile: Profile}) {
|
||||
const t = useT()
|
||||
const {profile} = props
|
||||
@@ -761,13 +785,69 @@ export function ProfileInterestsAndCauses(props: {profile: Profile}) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Personality card has anything to show. Exported so the card and its call site
|
||||
* (`profile/profile-info.tsx`) can't drift apart and render an empty card.
|
||||
*/
|
||||
export function hasPersonality(profile: Profile) {
|
||||
const p = profile as any
|
||||
return !!(
|
||||
profile.mbti ||
|
||||
profile.big5_agreeableness ||
|
||||
p.neurotype?.length ||
|
||||
p.neurotype_details
|
||||
)
|
||||
}
|
||||
|
||||
/** Whether the Accessibility card has anything to show. Shared with its call site, as with `hasPersonality`. */
|
||||
export function hasAccessibility(profile: Profile) {
|
||||
return !!(profile as any).accessibility_notes
|
||||
}
|
||||
|
||||
/**
|
||||
* Its own card below Personality rather than a row in Details: it is practical meet-up information, not a
|
||||
* demographic attribute, and it reads as an aside when buried in a list of them.
|
||||
*
|
||||
* Rendered as the member wrote it — no choice mapping, no chips, line breaks preserved. The whole point of
|
||||
* this field being free text is that the framing belongs to them, so the UI must not restructure it.
|
||||
*/
|
||||
export function ProfileAccessibility(props: {profile: Profile}) {
|
||||
const {profile} = props
|
||||
const notes = (profile as any).accessibility_notes as string | null | undefined
|
||||
|
||||
if (!notes) return null
|
||||
|
||||
return (
|
||||
<Row className="items-start gap-2.5">
|
||||
<div
|
||||
className="bg-canvas-100 border-canvas-200 text-ink-500 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border"
|
||||
style={{width: '32px', height: '32px', marginTop: '1px'}}
|
||||
>
|
||||
<Accessibility className="h-5 w-5" />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
color: 'rgb(var(--color-ink-900))',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{notes}
|
||||
</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfilePersonality(props: {profile: Profile}) {
|
||||
const {profile} = props
|
||||
|
||||
if (!profile.mbti && !profile.big5_agreeableness) return null
|
||||
if (!hasPersonality(profile)) return null
|
||||
|
||||
return (
|
||||
<Col className={clsx('relative gap-3 overflow-hidden rounded')}>
|
||||
{/* Grouped with MBTI/Big Five rather than with orientation in the Details card: it describes how
|
||||
someone's mind works, and matters just as much for friendship and collaboration as for dating. */}
|
||||
<Neurotype profile={profile} />
|
||||
<MBTI profile={profile} />
|
||||
<Big5Traits profile={profile} />
|
||||
</Col>
|
||||
|
||||
@@ -11,6 +11,9 @@ import {SignUpButton} from 'web/components/nav/sidebar'
|
||||
import {ConnectActions} from 'web/components/profile/connect-actions'
|
||||
import ProfileHeader, {ProfileHeaderActions} from 'web/components/profile/profile-header'
|
||||
import ProfileAbout, {
|
||||
hasAccessibility,
|
||||
hasPersonality,
|
||||
ProfileAccessibility,
|
||||
ProfileInterestsAndCauses,
|
||||
ProfileLinks,
|
||||
ProfilePersonality,
|
||||
@@ -352,12 +355,18 @@ function ProfileContent(props: {
|
||||
</ProfileCard>
|
||||
)}
|
||||
|
||||
{(profile.mbti || profile.big5_agreeableness) && (
|
||||
{hasPersonality(profile) && (
|
||||
<ProfileCard title={t('profile.personality', 'Personality')} className="p-5">
|
||||
<ProfilePersonality profile={profile} />
|
||||
</ProfileCard>
|
||||
)}
|
||||
|
||||
{hasAccessibility(profile) && (
|
||||
<ProfileCard title={t('profile.accessibility_notes', 'Accessibility')} className="p-5">
|
||||
<ProfileAccessibility profile={profile} />
|
||||
</ProfileCard>
|
||||
)}
|
||||
|
||||
{profile.links && Object.keys(profile.links).length > 0 && (
|
||||
<ProfileCard title={t('profile.links', 'Links')} className="p-5">
|
||||
<ProfileLinks profile={profile} />
|
||||
|
||||
Reference in New Issue
Block a user