Default exercises are created in selected language. Initial GUI added

This commit is contained in:
AG
2025-12-18 22:16:39 +02:00
parent 78d4a10f33
commit abffb52af1
12 changed files with 332 additions and 117 deletions

View File

@@ -8,6 +8,7 @@ import AICoach from './components/AICoach';
import Plans from './components/Plans';
import Login from './components/Login';
import Profile from './components/Profile';
import InitializeAccount from './components/InitializeAccount';
import { Language, User } from './types';
import { getSystemLanguage } from './services/i18n';
import { useAuth } from './context/AuthContext';
@@ -49,6 +50,10 @@ function App() {
return <Navigate to="/login" />;
}
if (currentUser?.isFirstLogin && location.pathname !== '/initialize') {
return <Navigate to="/initialize" />;
}
return (
<div className="h-[100dvh] w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden">
{currentUser && (
@@ -66,6 +71,17 @@ function App() {
<Navigate to="/" />
)
} />
<Route path="/initialize" element={
currentUser && currentUser.isFirstLogin ? (
<InitializeAccount
onInitialized={updateUser}
language={language}
onLanguageChange={setLanguage}
/>
) : (
<Navigate to="/" />
)
} />
<Route path="/" element={
<Tracker lang={language} />
} />

View File

@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { initializeAccount } from '../services/auth';
import { User, Language } from '../types';
import { Globe, ArrowRight, Check } from 'lucide-react';
import { t } from '../services/i18n';
interface InitializeAccountProps {
onInitialized: (user: User) => void;
language: Language;
onLanguageChange: (lang: Language) => void;
}
const InitializeAccount: React.FC<InitializeAccountProps> = ({ onInitialized, language, onLanguageChange }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const handleInitialize = async () => {
setIsSubmitting(true);
setError('');
const res = await initializeAccount(language);
if (res.success && res.user) {
onInitialized(res.user);
} else {
setError(res.error || 'Failed to initialize account');
setIsSubmitting(false);
}
};
const languages: { code: Language; label: string; desc: string }[] = [
{ code: 'en', label: 'English', desc: t('init_lang_en_desc', language) },
{ code: 'ru', label: 'Русский', desc: t('init_lang_ru_desc', language) },
];
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="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>
<h1 className="text-2xl font-normal text-on-surface mb-2 text-center">
{t('init_title', language)}
</h1>
<p className="text-sm text-on-surface-variant mb-8 text-center balance">
{t('init_desc', language)}
</p>
{error && (
<div className="w-full text-error text-sm text-center bg-error-container/10 p-3 rounded-xl mb-6 border border-error/10">
{error}
</div>
)}
<div className="w-full space-y-3 mb-8">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => onLanguageChange(lang.code)}
className={`w-full p-4 rounded-2xl border-2 transition-all flex items-center justify-between group ${language === lang.code
? 'border-primary bg-primary/5'
: 'border-outline-variant/30 hover:border-outline-variant hover:bg-surface-container-high'
}`}
>
<div className="text-left">
<div className={`font-medium ${language === lang.code ? 'text-primary' : 'text-on-surface'}`}>
{lang.label}
</div>
<div className="text-xs text-on-surface-variant">
{lang.desc}
</div>
</div>
{language === lang.code && (
<div className="w-6 h-6 bg-primary text-on-primary rounded-full flex items-center justify-center">
<Check size={14} strokeWidth={3} />
</div>
)}
</button>
))}
</div>
<button
onClick={handleInitialize}
disabled={isSubmitting}
className="w-full py-4 bg-primary text-on-primary rounded-full font-medium text-lg shadow-elevation-1 flex items-center justify-center gap-2 hover:shadow-elevation-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<div className="w-6 h-6 border-2 border-on-primary/30 border-t-on-primary rounded-full animate-spin" />
) : (
<>
{t('init_start', language)} <ArrowRight size={20} />
</>
)}
</button>
</div>
</div>
);
};
export default InitializeAccount;

View File

@@ -41,7 +41,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
if (tempUser && newPassword.length >= 4) {
const res = await changePassword(tempUser.id, newPassword);
if (res.success) {
const updatedUser = { ...tempUser, isFirstLogin: false };
const updatedUser = { ...tempUser };
onLogin(updatedUser);
} else {
setError(res.error || t('change_pass_error', language));

View File

@@ -149,3 +149,19 @@ 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 }> => {
try {
const res = await api.post<ApiResponse<{ user: User }>>('/auth/initialize', { language });
if (res.success && res.data) {
return { success: true, user: res.data.user };
}
return { success: false, error: res.error };
} catch (e: any) {
try {
const err = JSON.parse(e.message);
return { success: false, error: err.error || 'Failed to initialize account' };
} catch {
return { success: false, error: 'Failed to initialize account' };
}
}
};

View File

@@ -36,6 +36,12 @@ const translations = {
register_btn: 'Register',
have_account: 'Already have an account? Login',
need_account: 'Need an account? Register',
init_title: 'Setup Your Account',
init_desc: 'Welcome! Choose your preferred language.',
init_select_lang: 'Select Language',
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',
// General
date: 'Date',
@@ -253,6 +259,12 @@ const translations = {
register_btn: 'Зарегистрироваться',
have_account: 'Уже есть аккаунт? Войти',
need_account: 'Нет аккаунта? Регистрация',
init_title: 'Настройка аккаунта',
init_desc: 'Добро пожаловать! Выберите предпочтительный язык.',
init_select_lang: 'Выберите язык',
init_start: 'Начать работу',
init_lang_en_desc: 'Интерфейс и названия упражнений по умолчанию будут на английском',
init_lang_ru_desc: 'Интерфейс и названия упражнений по умолчанию будут на русском',
// General
date: 'Дата',