Initialize GUI has profile attributes

This commit is contained in:
AG
2025-12-18 22:45:50 +02:00
parent abffb52af1
commit 051e1e8a32
11 changed files with 136 additions and 19 deletions

View File

@@ -13,11 +13,22 @@ interface InitializeAccountProps {
const InitializeAccount: React.FC<InitializeAccountProps> = ({ onInitialized, language, onLanguageChange }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [birthDate, setBirthDate] = useState('');
const [height, setHeight] = useState('');
const [weight, setWeight] = useState('');
const [gender, setGender] = useState<'MALE' | 'FEMALE' | 'OTHER' | ''>('');
const handleInitialize = async () => {
setIsSubmitting(true);
setError('');
const res = await initializeAccount(language);
const profileData: any = {};
if (birthDate) profileData.birthDate = birthDate;
if (height) profileData.height = parseFloat(height);
if (weight) profileData.weight = parseFloat(weight);
if (gender) profileData.gender = gender;
const res = await initializeAccount(language, profileData);
if (res.success && res.user) {
onInitialized(res.user);
} else {
@@ -32,8 +43,8 @@ const InitializeAccount: React.FC<InitializeAccountProps> = ({ onInitialized, la
];
return (
<div className="h-screen bg-surface flex flex-col items-center justify-center p-6 bg-surface">
<div className="w-full max-w-sm bg-surface-container p-8 rounded-[28px] shadow-elevation-2 flex flex-col items-center">
<div className="min-h-screen bg-surface flex flex-col items-center justify-center p-6 sm:p-8">
<div className="w-full max-w-sm bg-surface-container p-8 rounded-[28px] shadow-elevation-2 flex flex-col items-center max-h-[90vh] overflow-y-auto custom-scrollbar">
<div className="w-16 h-16 bg-primary-container rounded-2xl flex items-center justify-center text-on-primary-container mb-6 shadow-elevation-1">
<Globe size={32} />
</div>
@@ -78,6 +89,59 @@ const InitializeAccount: React.FC<InitializeAccountProps> = ({ onInitialized, la
))}
</div>
<div className="w-full mb-8">
<div className="space-y-4 animate-in fade-in slide-in-from-top-2 duration-300">
<div>
<label htmlFor="birthDate" className="block text-xs text-on-surface-variant mb-1 ml-1">{t('birth_date', language)}</label>
<input
id="birthDate"
type="date"
value={birthDate}
onChange={(e) => setBirthDate(e.target.value)}
className="w-full p-4 rounded-2xl bg-surface-container-high border border-outline-variant/30 text-on-surface focus:border-primary focus:outline-none transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="height" className="block text-xs text-on-surface-variant mb-1 ml-1">{t('height', language)}</label>
<input
id="height"
type="number"
placeholder="cm"
value={height}
onChange={(e) => setHeight(e.target.value)}
className="w-full p-4 rounded-2xl bg-surface-container-high border border-outline-variant/30 text-on-surface focus:border-primary focus:outline-none transition-all"
/>
</div>
<div>
<label htmlFor="weight" className="block text-xs text-on-surface-variant mb-1 ml-1">{t('my_weight', language).split('(')[0].trim()}</label>
<input
id="weight"
type="number"
placeholder="kg"
value={weight}
onChange={(e) => setWeight(e.target.value)}
className="w-full p-4 rounded-2xl bg-surface-container-high border border-outline-variant/30 text-on-surface focus:border-primary focus:outline-none transition-all"
/>
</div>
</div>
<div>
<label htmlFor="gender" className="block text-xs text-on-surface-variant mb-1 ml-1">{t('gender', language)}</label>
<select
id="gender"
value={gender}
onChange={(e) => setGender(e.target.value as any)}
className="w-full p-4 rounded-2xl bg-surface-container-high border border-outline-variant/30 text-on-surface focus:border-primary focus:outline-none transition-all appearance-none"
>
<option value="">{t('select_gender', language)}</option>
<option value="MALE">{t('male', language)}</option>
<option value="FEMALE">{t('female', language)}</option>
<option value="OTHER">{t('other', language)}</option>
</select>
</div>
</div>
</div>
<button
onClick={handleInitialize}
disabled={isSubmitting}

View File

@@ -302,8 +302,9 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
<h3 className="text-sm font-bold text-primary mb-4">{t('personal_data', lang)}</h3>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Scale size={10} /> {t('weight_kg', lang)}</label>
<label htmlFor="profileWeight" className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Scale size={10} /> {t('weight_kg', lang)}</label>
<input
id="profileWeight"
data-testid="profile-weight-input"
type="number"
step="0.1"
@@ -313,8 +314,8 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
/>
</div>
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Ruler size={10} /> {t('height', lang)}</label>
<input data-testid="profile-height-input" type="number" value={height} onChange={(e) => setHeight(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none" />
<label htmlFor="profileHeight" className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Ruler size={10} /> {t('height', lang)}</label>
<input id="profileHeight" data-testid="profile-height-input" type="number" value={height} onChange={(e) => setHeight(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none" />
</div>
<div>
<DatePicker
@@ -326,8 +327,8 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
/>
</div>
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><PersonStanding size={10} /> {t('gender', lang)}</label>
<select data-testid="profile-gender" value={gender} onChange={(e) => setGender(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none text-sm py-1 bg-surface-container-high">
<label htmlFor="profileGender" className="text-[10px] text-on-surface-variant flex gap-1 items-center"><PersonStanding size={10} /> {t('gender', lang)}</label>
<select id="profileGender" data-testid="profile-gender" value={gender} onChange={(e) => setGender(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none text-sm py-1 bg-surface-container-high">
<option value="MALE">{t('male', lang)}</option>
<option value="FEMALE">{t('female', lang)}</option>
<option value="OTHER">{t('other', lang)}</option>

View File

@@ -429,7 +429,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
>
<label className={`
<label htmlFor={id} className={`
absolute top-2 left-4 text-label-sm font-medium transition-colors flex items-center gap-1
${isOpen ? 'text-primary' : 'text-on-surface-variant'}
`}>
@@ -437,6 +437,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
</label>
<input
id={id}
type="text"
value={textInputValue || formattedValue}
onChange={(e) => {

View File

@@ -149,9 +149,9 @@ export const getMe = async (): Promise<{ success: boolean; user?: User; error?:
return { success: false, error: 'Failed to fetch user' };
}
};
export const initializeAccount = async (language: string): Promise<{ success: boolean; user?: User; error?: string }> => {
export const initializeAccount = async (language: string, profile?: Partial<UserProfile>): Promise<{ success: boolean; user?: User; error?: string }> => {
try {
const res = await api.post<ApiResponse<{ user: User }>>('/auth/initialize', { language });
const res = await api.post<ApiResponse<{ user: User }>>('/auth/initialize', { language, ...profile });
if (res.success && res.data) {
return { success: true, user: res.data.user };
}

View File

@@ -42,6 +42,7 @@ const translations = {
init_start: 'Get Started',
init_lang_en_desc: 'GUI and default exercise names will be in English',
init_lang_ru_desc: 'GUI and default exercise names will be in Russian',
select_gender: 'Select Gender',
// General
date: 'Date',
@@ -265,6 +266,7 @@ const translations = {
init_start: 'Начать работу',
init_lang_en_desc: 'Интерфейс и названия упражнений по умолчанию будут на английском',
init_lang_ru_desc: 'Интерфейс и названия упражнений по умолчанию будут на русском',
select_gender: 'Выберите пол',
// General
date: 'Дата',