mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-09 15:44:55 -04:00
Translate profile forms
This commit is contained in:
3
common/src/parsing.ts
Normal file
3
common/src/parsing.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const toKey = (str: string) => {
|
||||
return str.replace(/ /g, '_').toLowerCase()
|
||||
}
|
||||
@@ -7,18 +7,20 @@ import {MultiCheckbox} from "web/components/multi-checkbox";
|
||||
import {capitalize} from "lodash";
|
||||
|
||||
export function AddOptionEntry(props: {
|
||||
title: string
|
||||
choices: { [key: string]: string }
|
||||
setChoices: (choices: any) => void
|
||||
profile: ProfileWithoutUser,
|
||||
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
|
||||
label: OptionTableKey,
|
||||
}) {
|
||||
const {profile, setProfile, label, choices, setChoices} = props
|
||||
const {profile, setProfile, label, choices, setChoices, title} = props
|
||||
return <Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>{capitalize(label)}</label>
|
||||
<label className={clsx(labelClassName)}>{title}</label>
|
||||
<MultiCheckbox
|
||||
choices={choices}
|
||||
selected={profile[label] ?? []}
|
||||
translationPrefix={`profile.${label}`}
|
||||
onChange={(selected) => setProfile(label, selected)}
|
||||
addOption={(v: string) => {
|
||||
console.log(`Adding ${label}:`, v)
|
||||
|
||||
@@ -219,6 +219,12 @@ export const MBTI_CHOICES = {
|
||||
'ESFP': 'esfp',
|
||||
}
|
||||
|
||||
export const GENDERS = {
|
||||
Woman: 'female',
|
||||
Man: 'male',
|
||||
Other: 'other',
|
||||
}
|
||||
|
||||
export const INVERTED_RELATIONSHIP_CHOICES = invert(RELATIONSHIP_CHOICES)
|
||||
export const INVERTED_RELATIONSHIP_STATUS_CHOICES = invert(RELATIONSHIP_STATUS_CHOICES)
|
||||
export const INVERTED_ROMANTIC_CHOICES = invert(ROMANTIC_CHOICES)
|
||||
@@ -228,4 +234,5 @@ export const INVERTED_EDUCATION_CHOICES = invert(EDUCATION_CHOICES)
|
||||
export const INVERTED_RELIGION_CHOICES = invert(RELIGION_CHOICES)
|
||||
export const INVERTED_LANGUAGE_CHOICES = invert(LANGUAGE_CHOICES)
|
||||
export const INVERTED_RACE_CHOICES = invert(RACE_CHOICES)
|
||||
export const INVERTED_MBTI_CHOICES = invert(MBTI_CHOICES)
|
||||
export const INVERTED_MBTI_CHOICES = invert(MBTI_CHOICES)
|
||||
export const INVERTED_GENDERS = invert(GENDERS)
|
||||
@@ -4,6 +4,8 @@ import { Input } from 'web/components/widgets/input'
|
||||
import { Button } from 'web/components/buttons/button'
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {useT} from "web/lib/locale";
|
||||
import {toKey} from "common/parsing";
|
||||
|
||||
export const MultiCheckbox = (props: {
|
||||
// Map of label -> value
|
||||
@@ -19,8 +21,9 @@ export const MultiCheckbox = (props: {
|
||||
// - null/undefined to indicate failure/cancellation
|
||||
addOption?: (label: string) => string | { key: string; value: string } | null | undefined
|
||||
addPlaceholder?: string
|
||||
translationPrefix?: string
|
||||
}) => {
|
||||
const { choices, selected, onChange, className, addOption, addPlaceholder } = props
|
||||
const { choices, selected, onChange, className, addOption, addPlaceholder, translationPrefix } = props
|
||||
|
||||
// Keep a local merged copy to allow optimistic adds while remaining in sync with props
|
||||
const [localChoices, setLocalChoices] = useState<{ [key: string]: string }>(choices)
|
||||
@@ -39,6 +42,8 @@ export const MultiCheckbox = (props: {
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const t = useT()
|
||||
|
||||
// Filter visible options while typing a new option (case-insensitive label match)
|
||||
const filteredEntries = useMemo(() => {
|
||||
if (!addOption) return entries
|
||||
@@ -90,7 +95,7 @@ export const MultiCheckbox = (props: {
|
||||
<Row className="items-center gap-2">
|
||||
<Input
|
||||
value={newLabel}
|
||||
placeholder={addPlaceholder ?? 'Search or add'}
|
||||
placeholder={addPlaceholder ?? t('multi-checkbox.search_or_add', 'Search or add')}
|
||||
onChange={(e) => {
|
||||
setNewLabel(e.target.value)
|
||||
setError(null)
|
||||
@@ -104,7 +109,7 @@ export const MultiCheckbox = (props: {
|
||||
className="h-10"
|
||||
/>
|
||||
<Button size="sm" onClick={submitAdd} loading={adding} disabled={adding}>
|
||||
Add
|
||||
{t('common.add', 'Add')}
|
||||
</Button>
|
||||
{error && <span className="text-sm text-error">{error}</span>}
|
||||
</Row>
|
||||
@@ -114,7 +119,7 @@ export const MultiCheckbox = (props: {
|
||||
{filteredEntries.map(([key, value]) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={key}
|
||||
label={t(`${translationPrefix}.${toKey(value)}`, key)}
|
||||
checked={selected.includes(value)}
|
||||
toggle={(checked: boolean) => {
|
||||
if (checked) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Title} from 'web/components/widgets/title'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import clsx from 'clsx'
|
||||
import {MultiCheckbox} from 'web/components/multi-checkbox'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {Input} from 'web/components/widgets/input'
|
||||
import {ChoicesToggleGroup} from 'web/components/widgets/choices-toggle-group'
|
||||
@@ -29,6 +30,7 @@ import {MultipleChoiceOptions} from "common/profiles/multiple-choice";
|
||||
import {
|
||||
DIET_CHOICES,
|
||||
EDUCATION_CHOICES,
|
||||
GENDERS,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
POLITICAL_CHOICES,
|
||||
@@ -42,6 +44,7 @@ import toast from "react-hot-toast";
|
||||
import {db} from "web/lib/supabase/db";
|
||||
import {fetchChoices} from "web/hooks/use-choices";
|
||||
import {AddOptionEntry} from "web/components/add-option-entry";
|
||||
import {convertGenderPlural, Gender} from "common/gender";
|
||||
|
||||
|
||||
export const OptionalProfileUserForm = (props: {
|
||||
@@ -57,6 +60,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [lookingRelationship, setLookingRelationship] = useState((profile.pref_relation_styles || []).includes('relationship'))
|
||||
const router = useRouter()
|
||||
const t = useT()
|
||||
const [heightFeet, setHeightFeet] = useState<number | undefined>(
|
||||
profile.height_in_inches
|
||||
? Math.floor((profile['height_in_inches'] ?? 0) / 12)
|
||||
@@ -180,17 +184,21 @@ export const OptionalProfileUserForm = (props: {
|
||||
{/* loading={isSubmitting}*/}
|
||||
{/* onClick={handleSubmit}*/}
|
||||
{/* >*/}
|
||||
{/* {buttonLabel ?? 'Next / Skip'}*/}
|
||||
{/* {buttonLabel ?? t('common.next', 'Next')} / {t('common.skip', 'Skip')}*/}
|
||||
{/* </Button>*/}
|
||||
{/*</Row>*/}
|
||||
|
||||
<Title>More about me</Title>
|
||||
<div className="text-ink-500 mb-6 text-lg">Optional information</div>
|
||||
<Title>{t('profile.optional.title', 'More about me')}</Title>
|
||||
<div className="text-ink-500 mb-6 text-lg">
|
||||
{t('profile.optional.subtitle', 'Optional information')}
|
||||
</div>
|
||||
|
||||
<Col className={'gap-8'}>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Location</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.location', 'Location')}
|
||||
</label>
|
||||
{profile.city ? (
|
||||
<Row className="border-primary-500 w-full justify-between rounded border px-4 py-2">
|
||||
<CityRow
|
||||
@@ -205,7 +213,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
setProfileCity(undefined)
|
||||
}}
|
||||
>
|
||||
Change
|
||||
{t('common.change', 'Change')}
|
||||
</button>
|
||||
</Row>
|
||||
) : (
|
||||
@@ -218,10 +226,12 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Age</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.age', 'Age')}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Age"
|
||||
placeholder={t('profile.optional.age', 'Age')}
|
||||
value={profile['age'] ?? undefined}
|
||||
min={18}
|
||||
max={100}
|
||||
@@ -231,37 +241,40 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
<Row className={'items-center gap-2'}>
|
||||
<Col className={'gap-1'}>
|
||||
<label className={clsx(labelClassName)}>Gender</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.gender', 'Gender')}
|
||||
</label>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={profile['gender']}
|
||||
choicesMap={{
|
||||
Woman: 'female',
|
||||
Man: 'male',
|
||||
Other: 'other',
|
||||
}}
|
||||
choicesMap={Object.fromEntries(Object.entries(GENDERS).map(([k, v]) => [t(`profile.gender.${v}`, k), v]))}
|
||||
setChoice={(c) => setProfile('gender', c)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Interested in connecting with</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.interested_in', 'Interested in connecting with')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={{
|
||||
Women: 'female',
|
||||
Men: 'male',
|
||||
Other: 'other',
|
||||
}}
|
||||
translationPrefix={'profile.gender.plural'}
|
||||
selected={profile['pref_gender'] || []}
|
||||
onChange={(selected) => setProfile('pref_gender', selected)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Who are aged between</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.age_range', 'Who are aged between')}
|
||||
</label>
|
||||
<Row className={'gap-2'}>
|
||||
<Col>
|
||||
<span>Min</span>
|
||||
<span>{t('common.min', 'Min')}</span>
|
||||
<Select
|
||||
value={profile['pref_age_min'] ?? ''}
|
||||
onChange={(e) =>
|
||||
@@ -278,7 +291,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Select>
|
||||
</Col>
|
||||
<Col>
|
||||
<span>Max</span>
|
||||
<span>{t('common.max', 'Max')}</span>
|
||||
<Select
|
||||
value={profile['pref_age_max'] ?? ''}
|
||||
onChange={(e) =>
|
||||
@@ -298,10 +311,13 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Connection type</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.connection_type', 'Connection type')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={RELATIONSHIP_CHOICES}
|
||||
selected={profile['pref_relation_styles'] || []}
|
||||
translationPrefix={'profile.relationship'}
|
||||
onChange={(selected) => {
|
||||
setProfile('pref_relation_styles', selected)
|
||||
setLookingRelationship((selected || []).includes('relationship'))
|
||||
@@ -310,9 +326,12 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Relationship status</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.relationship_status', 'Relationship status')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={RELATIONSHIP_STATUS_CHOICES}
|
||||
translationPrefix={'profile.relationship_status'}
|
||||
selected={profile['relationship_status'] ?? []}
|
||||
onChange={(selected) => setProfile('relationship_status', selected)}
|
||||
/>
|
||||
@@ -320,9 +339,12 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
{lookingRelationship && <>
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Relationship style</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.relationship_style', 'Relationship style')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={ROMANTIC_CHOICES}
|
||||
translationPrefix={'profile.romantic'}
|
||||
selected={profile['pref_romantic_styles'] || []}
|
||||
onChange={(selected) => {
|
||||
setProfile('pref_romantic_styles', selected)
|
||||
@@ -332,11 +354,11 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
I would like to have kids
|
||||
{t('profile.optional.want_kids', 'I would like to have kids')}
|
||||
</label>
|
||||
<RadioToggleGroup
|
||||
className={'w-44'}
|
||||
choicesMap={MultipleChoiceOptions}
|
||||
choicesMap={Object.fromEntries(Object.entries(MultipleChoiceOptions).map(([k, v]) => [t(`profile.wants_kids_${v}`, k), v]))}
|
||||
setChoice={(choice) => {
|
||||
setProfile('wants_kids_strength', choice)
|
||||
}}
|
||||
@@ -345,7 +367,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Current number of kids</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.num_kids', 'Current number of kids')}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
@@ -361,7 +385,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
</>}
|
||||
|
||||
<Col className={clsx(colClassName, 'pb-4')}>
|
||||
<label className={clsx(labelClassName)}>Socials</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.socials', 'Socials')}
|
||||
</label>
|
||||
|
||||
<div className="grid w-full grid-cols-[8rem_1fr_auto] gap-2">
|
||||
{Object.entries(newLinks)
|
||||
@@ -383,7 +409,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
<IconButton onClick={() => updateUserLink(platform, null)}>
|
||||
<XIcon className="h-6 w-6"/>
|
||||
<div className="sr-only">Remove</div>
|
||||
<div className="sr-only">
|
||||
{t('common.remove', 'Remove')}
|
||||
</div>
|
||||
</IconButton>
|
||||
</Fragment>
|
||||
))}
|
||||
@@ -401,8 +429,8 @@ export const OptionalProfileUserForm = (props: {
|
||||
placeholder={
|
||||
SITE_ORDER.includes(newLinkPlatform as any) &&
|
||||
newLinkPlatform != 'site'
|
||||
? 'Username or URL'
|
||||
: 'Site URL'
|
||||
? t('profile.optional.username_or_url', 'Username or URL')
|
||||
: t('profile.optional.site_url', 'Site URL')
|
||||
}
|
||||
value={newLinkValue}
|
||||
onChange={(e) => setNewLinkValue(e.target.value)}
|
||||
@@ -420,19 +448,24 @@ export const OptionalProfileUserForm = (props: {
|
||||
disabled={!newLinkPlatform || !newLinkValue}
|
||||
>
|
||||
<PlusIcon className="h-6 w-6"/>
|
||||
<div className="sr-only">Add</div>
|
||||
<div className="sr-only">
|
||||
{t('common.add', 'Add')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Languages</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.languages', 'Languages')}
|
||||
</label>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="col-span-full max-h-60 overflow-y-auto w-full">
|
||||
<MultiCheckbox
|
||||
choices={LANGUAGE_CHOICES}
|
||||
selected={profile.languages || []}
|
||||
translationPrefix={'profile.language'}
|
||||
onChange={(selected) => setProfile('languages', selected)}
|
||||
/>
|
||||
</div>
|
||||
@@ -441,13 +474,16 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Political beliefs</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.political_beliefs', 'Political beliefs')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={POLITICAL_CHOICES}
|
||||
selected={profile['political_beliefs'] ?? []}
|
||||
translationPrefix={'profile.political'}
|
||||
onChange={(selected) => setProfile('political_beliefs', selected)}
|
||||
/>
|
||||
<p>Details:</p>
|
||||
<p>{t('profile.optional.details', 'Details')}</p>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('political_details', e.target.value)}
|
||||
@@ -457,22 +493,26 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Religious beliefs</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.religious_beliefs', 'Religious beliefs')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={RELIGION_CHOICES}
|
||||
selected={profile['religion'] ?? []}
|
||||
translationPrefix={'profile.religion'}
|
||||
onChange={(selected) => setProfile('religion', selected)}
|
||||
/>
|
||||
<p>Details:</p>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('religious_beliefs', e.target.value)}
|
||||
className={'w-full sm:w-96'}
|
||||
value={profile['religious_beliefs'] ?? undefined}
|
||||
/>
|
||||
<p>{t('profile.optional.details', 'Details')}</p>
|
||||
</Col>
|
||||
|
||||
<AddOptionEntry
|
||||
title={t('profile.optional.interests', 'Interests')}
|
||||
choices={interestChoices}
|
||||
setChoices={setInterestChoices}
|
||||
profile={profile}
|
||||
@@ -481,6 +521,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
|
||||
<AddOptionEntry
|
||||
title={t('profile.optional.causes', 'Causes')}
|
||||
choices={causeChoices}
|
||||
setChoices={setCauseChoices}
|
||||
profile={profile}
|
||||
@@ -489,7 +530,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
|
||||
<Col className={clsx(colClassName, 'max-w-[550px]')}>
|
||||
<label className={clsx(labelClassName)}>MBTI Personality Type</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.mbti', 'MBTI Personality Type')}
|
||||
</label>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={profile['mbti'] ?? ''}
|
||||
choicesMap={MBTI_CHOICES}
|
||||
@@ -499,15 +542,19 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Diet</label>
|
||||
<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>
|
||||
|
||||
<AddOptionEntry
|
||||
title={t('profile.optional.work', 'Work')}
|
||||
choices={workChoices}
|
||||
setChoices={setWorkChoices}
|
||||
profile={profile}
|
||||
@@ -516,7 +563,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Company</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.company', 'Company')}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('company', e.target.value)}
|
||||
@@ -527,7 +576,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Job title {profile['company'] ? 'at ' + profile['company'] : ''}
|
||||
{profile['company']
|
||||
? t('profile.optional.job_title_at_company', 'Job title at {company}', {company: profile['company']})
|
||||
: t('profile.optional.job_title', 'Job title')}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -539,19 +590,21 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Highest completed education level
|
||||
{t('profile.optional.education_level', 'Highest completed education level')}
|
||||
</label>
|
||||
<Carousel className="max-w-full">
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={profile['education_level'] ?? ''}
|
||||
choicesMap={EDUCATION_CHOICES}
|
||||
choicesMap={Object.fromEntries(Object.entries(EDUCATION_CHOICES).map(([k, v]) => [t(`profile.education.${v}`, k), v]))}
|
||||
setChoice={(c) => setProfile('education_level', c)}
|
||||
/>
|
||||
</Carousel>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>University</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.university', 'University')}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('university', e.target.value)}
|
||||
@@ -561,20 +614,22 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Do you smoke?</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.smoke', 'Do you smoke?')}
|
||||
</label>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={profile['is_smoker'] ?? undefined}
|
||||
choicesMap={{
|
||||
choicesMap={Object.fromEntries(Object.entries({
|
||||
Yes: true,
|
||||
No: false,
|
||||
}}
|
||||
No: false
|
||||
}).map(([k, v]) => [t(`common.${k.toLowerCase()}`, k), v]))}
|
||||
setChoice={(c) => setProfile('is_smoker', c)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Alcoholic beverages consumed per month
|
||||
{t('profile.optional.drinks_per_month', 'Alcoholic beverages consumed per month')}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -590,10 +645,12 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Height</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.height', 'Height')}
|
||||
</label>
|
||||
<Row className={'gap-2'}>
|
||||
<Col>
|
||||
<span>Feet</span>
|
||||
<span>{t('profile.optional.feet', 'Feet')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
@@ -610,7 +667,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<span>Inches</span>
|
||||
<span>{t('profile.optional.inches', 'Inches')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
@@ -626,9 +683,11 @@ export const OptionalProfileUserForm = (props: {
|
||||
value={typeof heightInches === 'number' ? Math.floor(heightInches) : ''}
|
||||
/>
|
||||
</Col>
|
||||
<div className="self-end mb-2 text-ink-700 mx-2">OR</div>
|
||||
<div className="self-end mb-2 text-ink-700 mx-2">
|
||||
{t('common.or', 'OR').toUpperCase()}
|
||||
</div>
|
||||
<Col>
|
||||
<span>Centimeters</span>
|
||||
<span>{t('profile.optional.centimeters', 'Centimeters')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
@@ -664,9 +723,12 @@ export const OptionalProfileUserForm = (props: {
|
||||
</Col> */}
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Ethnicity/origin</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.ethnicity', 'Ethnicity/origin')}
|
||||
</label>
|
||||
<MultiCheckbox
|
||||
choices={RACE_CHOICES}
|
||||
translationPrefix={'profile.race'}
|
||||
selected={profile['ethnicity'] ?? []}
|
||||
onChange={(selected) => setProfile('ethnicity', selected)}
|
||||
/>
|
||||
@@ -682,7 +744,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
{/*</Col>*/}
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Photos</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.optional.photos', 'Photos')}
|
||||
</label>
|
||||
|
||||
{/*<div className="mb-1">*/}
|
||||
{/* A real or stylized photo of you is required.*/}
|
||||
@@ -712,7 +776,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{buttonLabel ?? 'Next'}
|
||||
{buttonLabel ?? t('common.next', 'Next')}
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -726,6 +790,7 @@ const CitySearchBox = (props: {
|
||||
// search results
|
||||
const {cities, query, setQuery} = useCitySearch()
|
||||
const [focused, setFocused] = useState(false)
|
||||
const t = useT()
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -734,7 +799,7 @@ const CitySearchBox = (props: {
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={'Search city...'}
|
||||
placeholder={t('profile.optional.search_city', 'Search city...')}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={(e) => {
|
||||
// Do not hide the dropdown if clicking inside the dropdown
|
||||
|
||||
@@ -33,6 +33,7 @@ import {MAX_INT, MIN_INT} from "common/constants";
|
||||
import {GiFruitBowl} from "react-icons/gi";
|
||||
import {FaBriefcase, FaHandsHelping, FaHeart, FaStar} from "react-icons/fa";
|
||||
import {useT} from "web/lib/locale";
|
||||
import {toKey} from "common/parsing";
|
||||
|
||||
export function AboutRow(props: {
|
||||
icon: ReactNode
|
||||
@@ -396,7 +397,3 @@ const capitalizeAndRemoveUnderscores = (str: string) => {
|
||||
const withSpaces = str.replace(/_/g, ' ')
|
||||
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1)
|
||||
}
|
||||
|
||||
const toKey = (str: string) => {
|
||||
return str.replace(/ /g, '_').toLowerCase()
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import {User} from 'common/user'
|
||||
import {useEditableUserInfo} from 'web/hooks/use-editable-user-info'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {ProfileRow, ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {SignupBio} from "web/components/bio/editable-bio";
|
||||
import {Editor} from "@tiptap/core";
|
||||
import {SignupBio} from "web/components/bio/editable-bio"
|
||||
import {Editor} from "@tiptap/core"
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export const initialRequiredState = {
|
||||
age: undefined,
|
||||
@@ -50,6 +51,7 @@ export const RequiredProfileUserForm = (props: {
|
||||
const {updateUsername, updateDisplayName, userInfo, updateUserState} = useEditableUserInfo(user)
|
||||
|
||||
const [step, setStep] = useState<number>(0)
|
||||
const t = useT()
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -84,12 +86,16 @@ export const RequiredProfileUserForm = (props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>The Basics</Title>
|
||||
<Title>{t('profile.basics.title', 'The Basics')}</Title>
|
||||
{step === 1 && !profileCreatedAlready &&
|
||||
<div className="text-ink-500 mb-6 text-lg">No endless forms—write your own bio, your own way.</div>}
|
||||
<div className="text-ink-500 mb-6 text-lg">
|
||||
{t('profile.basics.subtitle', 'No endless forms—write your own bio, your own way.')}
|
||||
</div>}
|
||||
<Col className={'gap-8 pb-[env(safe-area-inset-bottom)]'}>
|
||||
{step === 0 && <Col>
|
||||
<label className={clsx(labelClassName)}>Display name</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.basics.display_name', 'Display name')}
|
||||
</label>
|
||||
<Row className={'items-center gap-2'}>
|
||||
<Input
|
||||
disabled={loadingName}
|
||||
@@ -108,7 +114,9 @@ export const RequiredProfileUserForm = (props: {
|
||||
|
||||
{!profileCreatedAlready && <>
|
||||
{step === 0 && <Col>
|
||||
<label className={clsx(labelClassName)}>Username</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.basics.username', 'Username')}
|
||||
</label>
|
||||
<Row className={'items-center gap-2'}>
|
||||
<Input
|
||||
disabled={loadingUsername}
|
||||
@@ -128,7 +136,9 @@ export const RequiredProfileUserForm = (props: {
|
||||
</Col>}
|
||||
|
||||
{step === 1 && <Col>
|
||||
<label className={clsx(labelClassName)}>Bio</label>
|
||||
<label className={clsx(labelClassName)}>
|
||||
{t('profile.basics.bio', 'Bio')}
|
||||
</label>
|
||||
<SignupBio
|
||||
onChange={(e: Editor) => {
|
||||
console.debug('bio changed', e, profile.bio)
|
||||
@@ -153,7 +163,7 @@ export const RequiredProfileUserForm = (props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next
|
||||
{t('common.next', 'Next')}
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {PlusIcon, XIcon} from '@heroicons/react/solid'
|
||||
import Image from 'next/image'
|
||||
import {uniq} from 'lodash'
|
||||
import {useState} from 'react'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import {Col} from 'web/components/layout/col'
|
||||
@@ -22,6 +23,7 @@ export const AddPhotosWidget = (props: {
|
||||
setDescription: (url: string, description: string) => void
|
||||
}) => {
|
||||
const {user, photo_urls, pinned_url, setPhotoUrls, setPinnedUrl, setDescription, image_descriptions} = props
|
||||
const t = useT()
|
||||
|
||||
const [uploadingImages, setUploadingImages] = useState(false)
|
||||
|
||||
@@ -116,7 +118,7 @@ export const AddPhotosWidget = (props: {
|
||||
// stop click bubbling so clicking/focusing the input doesn't pin the image
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`description for image ${index}`}
|
||||
placeholder="Add description"
|
||||
placeholder={t('add_photos.add_description', 'Add description')}
|
||||
value={image_descriptions?.[url] ?? ''}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -132,7 +134,7 @@ export const AddPhotosWidget = (props: {
|
||||
</Row>
|
||||
{photo_urls?.length ? (
|
||||
<span className={'text-ink-500 text-xs italic'}>
|
||||
The highlighted image is your profile picture
|
||||
{t('add_photos.profile_picture_hint', 'The highlighted image is your profile picture')}
|
||||
</span>
|
||||
) : null}
|
||||
</Col>
|
||||
|
||||
@@ -293,6 +293,44 @@
|
||||
"privacy.effective_date": "Date d'entrée en vigueur : 1er janvier 2025",
|
||||
"privacy.intro.prefix": "Chez ",
|
||||
"privacy.intro.suffix": ", nous accordons de l'importance à la transparence et au respect de vos données. Cette politique de confidentialité explique comment nous traitons vos informations.",
|
||||
"profile.optional.title": "Plus d'informations à mon sujet",
|
||||
"profile.optional.subtitle": "Informations optionnelles",
|
||||
"profile.optional.location": "Localisation",
|
||||
"profile.optional.search_city": "Rechercher une ville...",
|
||||
"profile.optional.age": "Âge",
|
||||
"profile.optional.gender": "Genre",
|
||||
"profile.optional.interested_in": "Intéressé(e) par",
|
||||
"profile.optional.age_range": "Âgés entre",
|
||||
"profile.optional.connection_type": "Type de relation",
|
||||
"profile.optional.relationship_status": "Situation amoureuse",
|
||||
"profile.optional.relationship_style": "Style de relation",
|
||||
"profile.optional.want_kids": "Je souhaite avoir des enfants",
|
||||
"profile.optional.num_kids": "Nombre actuel d'enfants",
|
||||
"profile.optional.socials": "Réseaux sociaux",
|
||||
"profile.optional.username_or_url": "Nom d'utilisateur ou URL",
|
||||
"profile.optional.site_url": "URL du site",
|
||||
"profile.optional.languages": "Langues parlées",
|
||||
"profile.optional.political_beliefs": "Opinions politiques",
|
||||
"profile.optional.details": "Détails",
|
||||
"profile.optional.religious_beliefs": "Croyances religieuses",
|
||||
"profile.optional.mbti": "Type de personnalité MBTI",
|
||||
"profile.optional.diet": "Régime alimentaire",
|
||||
"profile.optional.company": "Entreprise",
|
||||
"profile.optional.job_title": "Poste actuel",
|
||||
"profile.optional.job_title_at_company": "Poste actuel chez {company}",
|
||||
"profile.optional.education_level": "Niveau d'études le plus élevé",
|
||||
"profile.optional.university": "Université",
|
||||
"profile.optional.smoke": "Fumez-vous ?",
|
||||
"profile.optional.drinks_per_month": "Boissons alcoolisées consommées par mois",
|
||||
"profile.optional.height": "Taille",
|
||||
"profile.optional.feet": "Pieds",
|
||||
"profile.optional.inches": "Pouces",
|
||||
"profile.optional.centimeters": "Centimètres",
|
||||
"profile.optional.ethnicity": "Origine ethnique",
|
||||
"profile.optional.photos": "Photos",
|
||||
"profile.optional.interests": "Intérets",
|
||||
"profile.optional.work": "Travail",
|
||||
"profile.optional.causes": "Causes morales",
|
||||
"privacy.info.title": "1. Informations que nous collectons",
|
||||
"privacy.info.text": "Nous collectons des informations de base de compte telles que votre nom, votre courriel et les données de profil. De plus, nous pouvons collecter des données d'utilisation pour améliorer les fonctionnalités de la plateforme.",
|
||||
"privacy.use.title": "2. Comment nous utilisons vos données",
|
||||
@@ -523,6 +561,12 @@
|
||||
"answers.importance.2": "Important",
|
||||
"answers.importance.3": "Très important",
|
||||
"answers.content.your_thoughts": "Vos pensées (optionnelles, mais recommendées)",
|
||||
"profile.basics.title": "Les bases",
|
||||
"profile.basics.subtitle": "Pas de formulaire sans fin — écrivez votre propre biographie à votre manière.",
|
||||
"profile.basics.display_name": "Nom d'affichage",
|
||||
"profile.basics.username": "Nom d'utilisateur",
|
||||
"profile.basics.bio": "Biographie",
|
||||
"common.next": "Suivant",
|
||||
"profile.interested_in": "Intéressé·e par",
|
||||
"profile.age_any": "de tout âge",
|
||||
"profile.age_exact": "exactement {min} ans",
|
||||
@@ -805,5 +849,12 @@
|
||||
"profile.comments.current_user_hint": "Les autres utilisateurs peuvent vous laisser des recommandations ici.",
|
||||
"profile.comments.other_user_hint": "Si vous les connaissez, écrivez quelque chose de sympa qui enrichira leur profil.",
|
||||
"profile.comments.feature_disabled_self": "Cette fonctionnalité est désactivée",
|
||||
"profile.comments.feature_disabled_other": "{name} a désactivé les recommandations des autres utilisateurs."
|
||||
"profile.comments.feature_disabled_other": "{name} a désactivé les recommandations des autres utilisateurs.",
|
||||
"multi-checkbox.search_or_add": "Chercher ou ajouter",
|
||||
"common.add": "Ajouter",
|
||||
"common.or": "Ou",
|
||||
"common.yes": "Oui",
|
||||
"common.no": "Non",
|
||||
"add_photos.add_description": "Ajouter une description",
|
||||
"add_photos.profile_picture_hint": "L'image mise en surbrillance est votre photo de profil"
|
||||
}
|
||||
Reference in New Issue
Block a user