mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-08-02 03:11:45 -04:00
Add exercise field to profiles; update filters, components, and database schema
This commit is contained in:
@@ -63,6 +63,10 @@ Database:
|
||||
yarn --cwd=backend/api regen-types-dev # rebuild common/src/supabase/schema.ts
|
||||
```
|
||||
|
||||
Every new migration must also be appended to `backend/supabase/migration.sql` (`\i
|
||||
backend/supabase/migrations/<file>.sql`, before the closing `COMMIT;`) — that ordered list is what rebuilds
|
||||
the schema from scratch.
|
||||
|
||||
## Adding an API endpoint (3 files across 2 packages)
|
||||
|
||||
1. **Schema** — add entry (method, `authed`, Zod `props`, `returns`) in `common/src/api/schema.ts`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"version": "1.56.0",
|
||||
"version": "1.57.0",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {type APIHandler} from 'api/helpers/endpoint'
|
||||
import {
|
||||
DIET_CHOICES,
|
||||
EDUCATION_CHOICES,
|
||||
EXERCISE_CHOICES,
|
||||
GENDERS,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
@@ -88,6 +89,7 @@ export type profileQueryType = {
|
||||
wants_kids_strength?: number | undefined
|
||||
has_kids?: number | undefined
|
||||
is_smoker?: boolean | undefined
|
||||
exercise?: string[] | undefined
|
||||
shortBio?: boolean | undefined
|
||||
psychedelics?: string[] | undefined
|
||||
cannabis?: string[] | undefined
|
||||
@@ -147,6 +149,7 @@ const choiceFields = [
|
||||
{field: 'gender', choices: GENDERS},
|
||||
{field: 'education_level', choices: EDUCATION_CHOICES},
|
||||
{field: 'mbti', choices: MBTI_CHOICES},
|
||||
{field: 'exercise', choices: EXERCISE_CHOICES},
|
||||
]
|
||||
|
||||
// Define array choice fields to search
|
||||
@@ -290,6 +293,7 @@ export const loadProfiles = async (props: profileQueryType, db?: SupabaseDirectC
|
||||
causes,
|
||||
work,
|
||||
is_smoker,
|
||||
exercise,
|
||||
shortBio,
|
||||
psychedelics,
|
||||
cannabis,
|
||||
@@ -629,6 +633,8 @@ export const loadProfiles = async (props: profileQueryType, db?: SupabaseDirectC
|
||||
{is_smoker},
|
||||
),
|
||||
|
||||
exercise?.length && where(`exercise = ANY($(exercise))`, {exercise}),
|
||||
|
||||
psychedelics?.length &&
|
||||
where(
|
||||
(psychedelics.includes('never_not_interested') ? 'psychedelics IS NULL OR ' : '') +
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CANNABIS_CHOICES,
|
||||
DIET_CHOICES,
|
||||
EDUCATION_CHOICES,
|
||||
EXERCISE_CHOICES,
|
||||
GENDERS,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
@@ -113,6 +114,7 @@ async function validateProfileFields(
|
||||
'gender',
|
||||
'education_level',
|
||||
'mbti',
|
||||
'exercise',
|
||||
'psychedelics',
|
||||
'cannabis',
|
||||
'headline',
|
||||
@@ -467,6 +469,7 @@ export async function callLLM(
|
||||
relationship_status: Object.values(RELATIONSHIP_STATUS_CHOICES),
|
||||
cannabis: Object.values(CANNABIS_CHOICES),
|
||||
education_level: Object.values(EDUCATION_CHOICES),
|
||||
exercise: Object.values(EXERCISE_CHOICES),
|
||||
gender: Object.values(GENDERS),
|
||||
mbti: Object.values(MBTI_CHOICES),
|
||||
psychedelics: Object.values(PSYCHEDELICS_CHOICES),
|
||||
@@ -511,6 +514,7 @@ export async function callLLM(
|
||||
'Number 0–4. How strongly they want kids (0 = definitely not, 4 = definitely yes).',
|
||||
diet: `Array. Any of: ${validChoices.diet?.join(', ')}`,
|
||||
ethnicity: `Array. Any of: ${validChoices.ethnicity?.join(', ')}`,
|
||||
exercise: `String. One of: ${validChoices.exercise?.join(', ')}. How often they exercise, only if explicitly stated.`,
|
||||
|
||||
// Substances
|
||||
psychedelics: `String. One of: ${validChoices.psychedelics?.join(', ')}. Usage frequency of psychedelics/plant medicine, only if explicitly stated.`,
|
||||
|
||||
@@ -7,13 +7,18 @@ Apply them with `./scripts/migrate.sh backend/supabase/migrations/<file>.sql` (s
|
||||
[`../../supabase/CLAUDE.md`](../../supabase/CLAUDE.md)). The `supabase/migrations/` folder at the repo root
|
||||
is generated/derived and gitignored — don't add migrations there.
|
||||
|
||||
**Every new migration must also be appended to [`migration.sql`](migration.sql)** as
|
||||
`\i backend/supabase/migrations/<file>.sql`, just before the closing `COMMIT;`. That file is the ordered
|
||||
list used to rebuild the schema from scratch — a migration missing from it silently never runs on a fresh
|
||||
database. Creating the migration file is not done until the line is added.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
backend/supabase/
|
||||
├── makefile Regen type / schema targets
|
||||
├── migrations/ Active migrations — YYYYMMDD_<name>.sql, applied in filename order (source of truth)
|
||||
├── migration.sql One-off SQL helpers
|
||||
├── migration.sql Ordered \i list of every table file + migration — rebuilds the schema from scratch
|
||||
├── users.sql, profiles.sql, ... Reference shapes — one file per table
|
||||
├── functions.sql, functions_others.sql Postgres functions
|
||||
├── extensions.sql Postgres extensions
|
||||
@@ -35,8 +40,9 @@ These targets are also exposed as `yarn --cwd=backend/api regen-types-dev` / `re
|
||||
## Conventions
|
||||
|
||||
- SQL is lowercase by convention across the codebase.
|
||||
- Adding a new table: create a new migration in [`migrations/`](migrations/), apply it (see
|
||||
`../../supabase/CLAUDE.md`), then run `make regen-types-dev` so the types in `common/` pick it up.
|
||||
- Adding a new table or column: create a new migration in [`migrations/`](migrations/), add its `\i` line to
|
||||
[`migration.sql`](migration.sql), apply it (see `../../supabase/CLAUDE.md`), then run `make regen-types-dev`
|
||||
so the types in `common/` pick it up.
|
||||
- Don't hand-edit `common/src/supabase/schema.ts` — it gets overwritten by `regen-types`.
|
||||
|
||||
## Related docs
|
||||
|
||||
@@ -56,7 +56,10 @@ 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
|
||||
\i backend/supabase/migrations/20260724_add_neurotype_to_profiles.sql
|
||||
\i backend/supabase/migrations/20260728_add_ban_reason_to_users.sql
|
||||
\i backend/supabase/migrations/20260730_cap_profiles_users_reads.sql
|
||||
\i backend/supabase/migrations/20260731_add_exercise_to_profiles.sql
|
||||
\i backend/supabase/migrations/20260731_lock_activity_stars_compat.sql
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Add exercise frequency to profiles
|
||||
-- Created: 2026-07-31
|
||||
--
|
||||
-- Scalar single-select (see EXERCISE_CHOICES in common/src/choices.ts): 'rarely' | 'sometimes' | 'often'.
|
||||
-- NULL means "not answered" — there is deliberately no "prefer not to say" choice.
|
||||
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS exercise TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS profiles_exercise_idx ON profiles (exercise);
|
||||
@@ -405,6 +405,7 @@
|
||||
"filter.age.years": "Jahre",
|
||||
"filter.any": "Alle",
|
||||
"filter.any_cannabis": "Cannabis",
|
||||
"filter.any_exercise": "Sport",
|
||||
"filter.any_causes": "Moralische Anliegen",
|
||||
"filter.any_diet": "Ernährung",
|
||||
"filter.any_drinks": "Getränke",
|
||||
@@ -448,6 +449,7 @@
|
||||
"filter.label.cannabis": "Cannabis",
|
||||
"filter.label.cannabis_intention": "Cannabis (Absicht)",
|
||||
"filter.label.diet": "Ernährung",
|
||||
"filter.label.exercise": "Sport",
|
||||
"filter.label.drinks": "Getränke",
|
||||
"filter.label.education_levels": "Bildung",
|
||||
"filter.label.has_kids": "Kinder",
|
||||
@@ -886,6 +888,10 @@
|
||||
"profile.connection.default": "Verbindung",
|
||||
"profile.connection_goals": "Verbindungsziele",
|
||||
"profile.diet": "Ernährung",
|
||||
"profile.exercise": "Sport",
|
||||
"profile.exercise.rarely": "Einmal pro Woche oder weniger",
|
||||
"profile.exercise.sometimes": "2–4 Mal pro Woche",
|
||||
"profile.exercise.often": "5+ Mal pro Woche",
|
||||
"profile.diet.keto": "Ketogen",
|
||||
"profile.diet.omnivore": "Omnivor",
|
||||
"profile.diet.other": "Andere",
|
||||
@@ -1160,6 +1166,7 @@
|
||||
"profile.optional.category.family": "Familie",
|
||||
"profile.optional.category.interested_in": "Wen ich suche",
|
||||
"profile.optional.category.morality": "Moral",
|
||||
"profile.optional.category.lifestyle": "Lebensstil",
|
||||
"profile.optional.category.personal_info": "Persönliche Informationen",
|
||||
"profile.optional.category.photos": "Fotos",
|
||||
"profile.optional.category.psychology": "Psychologie",
|
||||
@@ -1172,6 +1179,7 @@
|
||||
"profile.optional.connection_type": "Beziehungsart",
|
||||
"profile.optional.details": "Details",
|
||||
"profile.optional.diet": "Ernährungsweise",
|
||||
"profile.optional.exercise": "Sport",
|
||||
"profile.optional.drinks_per_month": "Alkoholische Getränke pro Monat",
|
||||
"profile.optional.education_level": "Höchster Bildungsabschluss",
|
||||
"profile.optional.error.invalid_fields": "Einige Felder sind nicht korrekt...",
|
||||
|
||||
@@ -405,6 +405,7 @@
|
||||
"filter.age.years": "ans",
|
||||
"filter.any": "Tous",
|
||||
"filter.any_cannabis": "Cannabis",
|
||||
"filter.any_exercise": "Activité physique",
|
||||
"filter.any_causes": "Causes morales",
|
||||
"filter.any_diet": "Régime alimentaire",
|
||||
"filter.any_drinks": "Boissons",
|
||||
@@ -448,6 +449,7 @@
|
||||
"filter.label.cannabis": "Cannabis",
|
||||
"filter.label.cannabis_intention": "Cannabis (intentions)",
|
||||
"filter.label.diet": "Régime",
|
||||
"filter.label.exercise": "Activité physique",
|
||||
"filter.label.drinks": "Boissons",
|
||||
"filter.label.education_levels": "Éducation",
|
||||
"filter.label.has_kids": "Enfants",
|
||||
@@ -885,6 +887,10 @@
|
||||
"profile.connection.default": "une relation",
|
||||
"profile.connection_goals": "Objectifs de relation",
|
||||
"profile.diet": "Régime alimentaire",
|
||||
"profile.exercise": "Activité physique",
|
||||
"profile.exercise.rarely": "Une fois par semaine ou moins",
|
||||
"profile.exercise.sometimes": "2 à 4 fois par semaine",
|
||||
"profile.exercise.often": "5 fois ou plus par semaine",
|
||||
"profile.diet.keto": "Cétogène",
|
||||
"profile.diet.omnivore": "Omnivore",
|
||||
"profile.diet.other": "Autre",
|
||||
@@ -1159,6 +1165,7 @@
|
||||
"profile.optional.category.family": "Famille",
|
||||
"profile.optional.category.interested_in": "Ce que je recherche",
|
||||
"profile.optional.category.morality": "Moralité",
|
||||
"profile.optional.category.lifestyle": "Mode de vie",
|
||||
"profile.optional.category.personal_info": "Infos personnelles",
|
||||
"profile.optional.category.photos": "Photos",
|
||||
"profile.optional.category.psychology": "Psychologie",
|
||||
@@ -1171,6 +1178,7 @@
|
||||
"profile.optional.connection_type": "Type de relation",
|
||||
"profile.optional.details": "Détails",
|
||||
"profile.optional.diet": "Régime alimentaire",
|
||||
"profile.optional.exercise": "Activité physique",
|
||||
"profile.optional.drinks_per_month": "Boissons alcoolisées consommées par mois",
|
||||
"profile.optional.education_level": "Niveau d'études le plus élevé",
|
||||
"profile.optional.error.invalid_fields": "Certains champs sont incorrects...",
|
||||
|
||||
@@ -788,6 +788,7 @@ export const API = (_apiTypeCheck = {
|
||||
wants_kids_strength: z.coerce.number().optional(),
|
||||
has_kids: z.coerce.number().optional(),
|
||||
is_smoker: zBoolean.optional().optional(),
|
||||
exercise: arraybeSchema.optional(),
|
||||
psychedelics: arraybeSchema.optional(),
|
||||
cannabis: arraybeSchema.optional(),
|
||||
psychedelics_intention: arraybeSchema.optional(),
|
||||
|
||||
@@ -99,6 +99,7 @@ const optionalProfilesSchema = z.object({
|
||||
image_descriptions: z.any().optional().nullable(),
|
||||
interests: z.array(z.string()).optional().nullable(),
|
||||
is_smoker: zBoolean.optional().nullable(),
|
||||
exercise: z.string().optional().nullable(),
|
||||
links: z.record(linkValueSchema).optional(),
|
||||
mbti: z.string().optional().nullable(),
|
||||
occupation: z.string().optional().nullable(),
|
||||
|
||||
@@ -47,6 +47,14 @@ export const DIET_CHOICES = {
|
||||
Other: 'other',
|
||||
} as const
|
||||
|
||||
// Frequency buckets rather than intensity or identity labels: "active"/"moderate" are self-graded and
|
||||
// drift a full bucket between users, whereas sessions-per-week reads the same to everyone.
|
||||
export const EXERCISE_CHOICES = {
|
||||
'Once a week or less': 'rarely',
|
||||
'2–4 times a week': 'sometimes',
|
||||
'5+ times a week': 'often',
|
||||
} as const
|
||||
|
||||
export const EDUCATION_CHOICES = {
|
||||
'High school': 'high-school',
|
||||
College: 'some-college',
|
||||
@@ -384,6 +392,7 @@ export const INVERTED_RELATIONSHIP_STATUS_CHOICES = invert(RELATIONSHIP_STATUS_C
|
||||
export const INVERTED_ROMANTIC_CHOICES = invert(ROMANTIC_CHOICES)
|
||||
export const INVERTED_POLITICAL_CHOICES = invert(POLITICAL_CHOICES)
|
||||
export const INVERTED_DIET_CHOICES = invert(DIET_CHOICES)
|
||||
export const INVERTED_EXERCISE_CHOICES = invert(EXERCISE_CHOICES)
|
||||
export const INVERTED_EDUCATION_CHOICES = invert(EDUCATION_CHOICES)
|
||||
export const INVERTED_RELIGION_CHOICES = invert(RELIGION_CHOICES)
|
||||
export const INVERTED_LANGUAGE_CHOICES = invert(LANGUAGE_CHOICES)
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
INVERTED_CANNABIS_CHOICES,
|
||||
INVERTED_DIET_CHOICES,
|
||||
INVERTED_EDUCATION_CHOICES,
|
||||
INVERTED_EXERCISE_CHOICES,
|
||||
INVERTED_GENDERS,
|
||||
INVERTED_LANGUAGE_CHOICES,
|
||||
INVERTED_MBTI_CHOICES,
|
||||
@@ -35,6 +36,7 @@ const filterLabels: Record<string, string> = {
|
||||
has_kids: '',
|
||||
wants_kids_strength: '',
|
||||
is_smoker: '',
|
||||
exercise: 'Exercise',
|
||||
pref_relation_styles: '',
|
||||
pref_romantic_styles: '',
|
||||
interests: '',
|
||||
@@ -198,6 +200,10 @@ export function formatFilters(
|
||||
value = value.map((s) => translate(`profile.gender.${s}`, INVERTED_GENDERS[s]))
|
||||
} else if (key === 'genders') {
|
||||
value = value.map((s) => translate(`profile.gender.${s}`, INVERTED_GENDERS[s]))
|
||||
} else if (key === 'exercise') {
|
||||
value = value.map((s) =>
|
||||
translate(`profile.exercise.${s}`, INVERTED_EXERCISE_CHOICES[s] ?? s),
|
||||
)
|
||||
} else if (key === 'cannabis') {
|
||||
value = value.map((s) => translate(`profile.cannabis.${s}`, INVERTED_CANNABIS_CHOICES[s]))
|
||||
} else if (key === 'psychedelics') {
|
||||
|
||||
@@ -16,6 +16,8 @@ export type FilterFields = {
|
||||
genders: string[] | null | undefined
|
||||
cannabis: string[] | null | undefined
|
||||
psychedelics: string[] | null | undefined
|
||||
// Scalar on the profile, multi-select as a filter — same shape as `cannabis` above.
|
||||
exercise: string[] | null | undefined
|
||||
education_levels: string[] | null | undefined
|
||||
mbti: string[] | null | undefined
|
||||
name: string | null | undefined
|
||||
@@ -91,6 +93,7 @@ export const initialFilters: Partial<FilterFields> = {
|
||||
has_kids: undefined,
|
||||
wants_kids_strength: undefined,
|
||||
is_smoker: undefined,
|
||||
exercise: undefined,
|
||||
psychedelics: undefined,
|
||||
cannabis: undefined,
|
||||
psychedelics_intention: undefined,
|
||||
|
||||
@@ -1192,6 +1192,7 @@ export type Database = {
|
||||
drinks_per_month: number | null
|
||||
education_level: string | null
|
||||
ethnicity: string[] | null
|
||||
exercise: string | null
|
||||
gender: string | null
|
||||
gender_details: string | null
|
||||
geodb_city_id: string | null
|
||||
@@ -1276,6 +1277,7 @@ export type Database = {
|
||||
drinks_per_month?: number | null
|
||||
education_level?: string | null
|
||||
ethnicity?: string[] | null
|
||||
exercise?: string | null
|
||||
gender?: string | null
|
||||
gender_details?: string | null
|
||||
geodb_city_id?: string | null
|
||||
@@ -1360,6 +1362,7 @@ export type Database = {
|
||||
drinks_per_month?: number | null
|
||||
education_level?: string | null
|
||||
ethnicity?: string[] | null
|
||||
exercise?: string | null
|
||||
gender?: string | null
|
||||
gender_details?: string | null
|
||||
geodb_city_id?: string | null
|
||||
@@ -1877,6 +1880,7 @@ export type Database = {
|
||||
drinks_per_month: number | null
|
||||
education_level: string | null
|
||||
ethnicity: string[] | null
|
||||
exercise: string | null
|
||||
gender: string | null
|
||||
gender_details: string | null
|
||||
geodb_city_id: string | null
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CANNABIS_CHOICES,
|
||||
DIET_CHOICES,
|
||||
EDUCATION_CHOICES,
|
||||
EXERCISE_CHOICES,
|
||||
GENDERS,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
@@ -60,6 +61,7 @@ class UserAccountInformationForSeeding {
|
||||
political_beliefs = Object.values(POLITICAL_CHOICES)
|
||||
religion = Object.values(RELIGION_CHOICES)
|
||||
diet = Object.values(DIET_CHOICES)
|
||||
exercise = Object.values(EXERCISE_CHOICES)
|
||||
drinks_per_month = faker.number.int({min: 4, max: 40})
|
||||
height_in_inches = faker.number.float({min: 56, max: 78, fractionDigits: 2})
|
||||
ethnicity = Object.values(RACE_CHOICES)
|
||||
|
||||
@@ -104,6 +104,7 @@ export async function seedDbUser(
|
||||
|
||||
const fullProfile = {
|
||||
...mediumProfile,
|
||||
exercise: userInfo.randomElement(userInfo.exercise),
|
||||
cannabis: userInfo.randomElement(userInfo.cannabis),
|
||||
psychedelics: userInfo.randomElement(userInfo.psychedelics),
|
||||
cannabis_intention: [userInfo.randomElement(userInfo.cannabis_intention)],
|
||||
|
||||
64
web/components/filters/exercise-filter.tsx
Normal file
64
web/components/filters/exercise-filter.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import clsx from 'clsx'
|
||||
import {EXERCISE_CHOICES} from 'common/choices'
|
||||
import {FilterFields} from 'common/filters'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {MultiCheckbox} from 'web/components/multi-checkbox'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export function ExerciseFilterText(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_exercise', 'Exercise')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (length > 1) {
|
||||
return (
|
||||
<span>
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
{t('filter.multiple', 'Multiple')}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const option = options[0]
|
||||
const label = Object.entries(EXERCISE_CHOICES).find(([_, v]) => v === option)?.[0] || option
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
{t(`profile.exercise.${option}`, label)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ExerciseFilter(props: {
|
||||
filters: Partial<FilterFields>
|
||||
updateFilter: (newState: Partial<FilterFields>) => void
|
||||
}) {
|
||||
const {filters, updateFilter} = props
|
||||
return (
|
||||
<Col className="gap-4">
|
||||
<MultiCheckbox
|
||||
selected={filters.exercise ?? []}
|
||||
choices={EXERCISE_CHOICES as any}
|
||||
translationPrefix={'profile.exercise'}
|
||||
onChange={(c) => {
|
||||
updateFilter({exercise: c})
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {CardSizeSelector} from 'web/components/filters/card-size-selector'
|
||||
import {DietFilter, DietFilterText} from 'web/components/filters/diet-filter'
|
||||
import {EducationFilter, EducationFilterText} from 'web/components/filters/education-filter'
|
||||
import {ExerciseFilter, ExerciseFilterText} from 'web/components/filters/exercise-filter'
|
||||
import {FieldToggles} from 'web/components/filters/field-toggles'
|
||||
import {HasPhotoFilter} from 'web/components/filters/has-photo-filter'
|
||||
import {IncompleteProfilesToggle} from 'web/components/filters/incomplete-profiles-toggle'
|
||||
@@ -525,6 +526,23 @@ function Filters(props: {
|
||||
locationFilterProps={raisedInLocationFilterProps}
|
||||
/>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection
|
||||
title={t('profile.optional.languages', 'Languages')}
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.languages || undefined)}
|
||||
selection={
|
||||
<LanguageFilterText
|
||||
options={filters.languages as string[] | undefined}
|
||||
// highlightedClass={
|
||||
// hasAny(filters.languages || undefined) ? 'text-primary-600' : 'text-ink-900'
|
||||
// }
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LanguageFilter filters={filters} updateFilter={updateFilter} />
|
||||
</FilterSection>
|
||||
</FilterGroup>
|
||||
|
||||
{/* Lifestyle Group */}
|
||||
@@ -574,6 +592,16 @@ function Filters(props: {
|
||||
<DietFilter filters={filters} updateFilter={updateFilter} />
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection
|
||||
title={t('profile.optional.exercise', 'Exercise')}
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.exercise || undefined)}
|
||||
selection={<ExerciseFilterText options={filters.exercise as string[] | undefined} />}
|
||||
>
|
||||
<ExerciseFilter filters={filters} updateFilter={updateFilter} />
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection
|
||||
title={t('profile.optional.drinks_per_month', 'Drinks')}
|
||||
openFilter={openFilter}
|
||||
@@ -651,23 +679,6 @@ function Filters(props: {
|
||||
>
|
||||
<CannabisFilter filters={filters} updateFilter={updateFilter} />
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection
|
||||
title={t('profile.optional.languages', 'Languages')}
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.languages || undefined)}
|
||||
selection={
|
||||
<LanguageFilterText
|
||||
options={filters.languages as string[] | undefined}
|
||||
// highlightedClass={
|
||||
// hasAny(filters.languages || undefined) ? 'text-primary-600' : 'text-ink-900'
|
||||
// }
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LanguageFilter filters={filters} updateFilter={updateFilter} />
|
||||
</FilterSection>
|
||||
</FilterGroup>
|
||||
|
||||
{/* Values & Beliefs Group */}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DEFAULT_ORIENTATIONS,
|
||||
DIET_CHOICES,
|
||||
EDUCATION_CHOICES,
|
||||
EXERCISE_CHOICES,
|
||||
EXTRA_NEUROTYPES,
|
||||
GENDERS,
|
||||
GENDERS_PLURAL,
|
||||
@@ -1158,20 +1159,6 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.diet', 'Diet')} />
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
{/*<label className={clsx(labelClassName)}>*/}
|
||||
{/* {t('profile.optional.diet', 'Diet')}*/}
|
||||
{/*</label>*/}
|
||||
<MultiCheckbox
|
||||
choices={DIET_CHOICES}
|
||||
selected={profile['diet'] ?? []}
|
||||
translationPrefix={'profile.diet'}
|
||||
onChange={(selected) => setProfile('diet', selected)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.category.morality', 'Morality')} />
|
||||
<AddOptionEntry
|
||||
title={t('profile.optional.causes', 'Causes')}
|
||||
@@ -1192,6 +1179,38 @@ export const OptionalProfileUserForm = (props: {
|
||||
label={'interests'}
|
||||
/>
|
||||
|
||||
<Category title={t('profile.optional.category.lifestyle', 'Lifestyle')} />
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>{t('profile.optional.diet', 'Diet')}</label>
|
||||
<MultiCheckbox
|
||||
choices={DIET_CHOICES}
|
||||
selected={profile['diet'] ?? []}
|
||||
translationPrefix={'profile.diet'}
|
||||
onChange={(selected) => setProfile('diet', selected)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.exercise', 'Exercise')}
|
||||
</label>
|
||||
<Select
|
||||
value={profile['exercise'] ?? ''}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setProfile('exercise', e.target.value || null)
|
||||
}
|
||||
className={'w-full sm:w-80'}
|
||||
>
|
||||
<option value=""></option>
|
||||
{Object.entries(EXERCISE_CHOICES).map(([label, value]) => (
|
||||
<option key={value} value={value}>
|
||||
{t(`profile.exercise.${value}`, label)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
|
||||
<Category title={t('profile.optional.category.substances', 'Substances')} />
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
INVERTED_CANNABIS_CHOICES,
|
||||
INVERTED_DIET_CHOICES,
|
||||
INVERTED_EDUCATION_CHOICES,
|
||||
INVERTED_EXERCISE_CHOICES,
|
||||
INVERTED_LANGUAGE_CHOICES,
|
||||
INVERTED_MBTI_CHOICES,
|
||||
INVERTED_NEUROTYPE_CHOICES,
|
||||
@@ -77,6 +78,7 @@ export default function ProfileAbout(props: {
|
||||
<Politics profile={profile} />
|
||||
<Religion profile={profile} />
|
||||
<Diet profile={profile} />
|
||||
<Exercise profile={profile} />
|
||||
<Smoker profile={profile} />
|
||||
<Drinks profile={profile} />
|
||||
<Cannabis profile={profile} />
|
||||
@@ -632,6 +634,21 @@ function Diet(props: {profile: Profile}) {
|
||||
)
|
||||
}
|
||||
|
||||
function Exercise(props: {profile: Profile}) {
|
||||
const t = useT()
|
||||
const {profile} = props
|
||||
const exercise = profile.exercise
|
||||
|
||||
if (!exercise) return null
|
||||
|
||||
return (
|
||||
<AboutRow
|
||||
title={t('profile.exercise', 'Exercise')}
|
||||
text={t(`profile.exercise.${exercise}`, INVERTED_EXERCISE_CHOICES[exercise] ?? exercise)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function LanguagesSection(props: {profile: Profile}) {
|
||||
const t = useT()
|
||||
const {profile} = props
|
||||
|
||||
@@ -127,7 +127,13 @@ export function RangeSlider(props: {
|
||||
|
||||
return (
|
||||
<RxSlider.Root
|
||||
className={clsx(className, 'relative flex h-7 touch-none select-none items-center')}
|
||||
className={clsx(
|
||||
className,
|
||||
'relative flex h-7 touch-none select-none items-center',
|
||||
// mark labels are absolutely positioned below the track, so they'd otherwise spill past
|
||||
// the container's own padding and get clipped
|
||||
marks && 'mb-5',
|
||||
)}
|
||||
value={dragValues}
|
||||
step={step ?? 1}
|
||||
onValueChange={(vals: number[]) => setDragValues([vals[0], vals[1]])} // update continuously for UI feedback
|
||||
|
||||
Reference in New Issue
Block a user