Translate profile forms

This commit is contained in:
MartinBraquet
2026-01-03 13:18:21 +02:00
parent 4abed529d3
commit 9a0f0c0892
9 changed files with 218 additions and 76 deletions

3
common/src/parsing.ts Normal file
View File

@@ -0,0 +1,3 @@
export const toKey = (str: string) => {
return str.replace(/ /g, '_').toLowerCase()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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