Default exercises are created in selected language. Initial GUI added
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -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} />
|
||||
} />
|
||||
|
||||
99
src/components/InitializeAccount.tsx
Normal file
99
src/components/InitializeAccount.tsx
Normal 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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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: 'Дата',
|
||||
|
||||
Reference in New Issue
Block a user