Default exercises are created in selected language. Initial GUI added
This commit is contained in:
@@ -1,43 +1,43 @@
|
|||||||
name,type,bodyWeightPercentage,isUnilateral
|
name,name_ru,type,bodyWeightPercentage,isUnilateral
|
||||||
Air Squats,BODYWEIGHT,1.0,false
|
Air Squats,Приседания,BODYWEIGHT,1.0,false
|
||||||
Barbell Row,STRENGTH,0,false
|
Barbell Row,Тяга штанги в наклоне,STRENGTH,0,false
|
||||||
Bench Press,STRENGTH,0,false
|
Bench Press,Жим лежа,STRENGTH,0,false
|
||||||
Bicep Curl,STRENGTH,0,true
|
Bicep Curl,Подъем на бицепс,STRENGTH,0,true
|
||||||
Bulgarian Split-Squat Jumps,BODYWEIGHT,1.0,true
|
Bulgarian Split-Squat Jumps,Болгарские сплит-прыжки,BODYWEIGHT,1.0,true
|
||||||
Bulgarian Split-Squats,BODYWEIGHT,1.0,true
|
Bulgarian Split-Squats,Болгарские сплит-приседания,BODYWEIGHT,1.0,true
|
||||||
Burpees,BODYWEIGHT,1.0,false
|
Burpees,Берпи,BODYWEIGHT,1.0,false
|
||||||
Calf Raise,STRENGTH,0,true
|
Calf Raise,Подъем на носки,STRENGTH,0,true
|
||||||
Chin-Ups,BODYWEIGHT,1.0,false
|
Chin-Ups,Подтягивания обратным хватом,BODYWEIGHT,1.0,false
|
||||||
Cycling,CARDIO,0,false
|
Cycling,Велосипед,CARDIO,0,false
|
||||||
Deadlift,STRENGTH,0,false
|
Deadlift,Становая тяга,STRENGTH,0,false
|
||||||
Dips,BODYWEIGHT,1.0,false
|
Dips,Отжимания на брусьях,BODYWEIGHT,1.0,false
|
||||||
Dumbbell Curl,STRENGTH,0,true
|
Dumbbell Curl,Сгибания рук с гантелями,STRENGTH,0,true
|
||||||
Dumbbell Shoulder Press,STRENGTH,0,true
|
Dumbbell Shoulder Press,Жим гантелей сидя,STRENGTH,0,true
|
||||||
Face Pull,STRENGTH,0,false
|
Face Pull,Тяга к лицу,STRENGTH,0,false
|
||||||
Front Squat,STRENGTH,0,false
|
Front Squat,Фронтальный присед,STRENGTH,0,false
|
||||||
Hammer Curl,STRENGTH,0,true
|
Hammer Curl,Сгибания "Молот",STRENGTH,0,true
|
||||||
Handstand,BODYWEIGHT,1.0,false
|
Handstand,Стойка на руках,BODYWEIGHT,1.0,false
|
||||||
Hip Thrust,STRENGTH,0,false
|
Hip Thrust,Ягодичный мостик,STRENGTH,0,false
|
||||||
Jump Rope,CARDIO,0,false
|
Jump Rope,Скакалка,CARDIO,0,false
|
||||||
Lat Pulldown,STRENGTH,0,false
|
Lat Pulldown,Тяга верхнего блока,STRENGTH,0,false
|
||||||
Leg Extension,STRENGTH,0,true
|
Leg Extension,Разгибание ног в тренажере,STRENGTH,0,true
|
||||||
Leg Press,STRENGTH,0,false
|
Leg Press,Жим ногами,STRENGTH,0,false
|
||||||
Lunges,BODYWEIGHT,1.0,true
|
Lunges,Выпады,BODYWEIGHT,1.0,true
|
||||||
Mountain Climbers,CARDIO,0,false
|
Mountain Climbers,Альпинист,CARDIO,0,false
|
||||||
Muscle-Up,BODYWEIGHT,1.0,false
|
Muscle-Up,Выход силой,BODYWEIGHT,1.0,false
|
||||||
Overhead Press,STRENGTH,0,false
|
Overhead Press,Армейский жим,STRENGTH,0,false
|
||||||
Plank,STATIC,0,false
|
Plank,Планка,STATIC,0,false
|
||||||
Pull-Ups,BODYWEIGHT,1.0,false
|
Pull-Ups,Подтягивания,BODYWEIGHT,1.0,false
|
||||||
Push-Ups,BODYWEIGHT,0.65,false
|
Push-Ups,Отжимания,BODYWEIGHT,0.65,false
|
||||||
Romanian Deadlift,STRENGTH,0,false
|
Romanian Deadlift,Румынская тяга,STRENGTH,0,false
|
||||||
Rowing,CARDIO,0,false
|
Rowing,Гребля,CARDIO,0,false
|
||||||
Running,CARDIO,0,false
|
Running,Бег,CARDIO,0,false
|
||||||
Russian Twist,BODYWEIGHT,0,false
|
Russian Twist,Русский твист,BODYWEIGHT,0,false
|
||||||
Seated Cable Row,STRENGTH,0,false
|
Seated Cable Row,Тяга блока к поясу,STRENGTH,0,false
|
||||||
Side Plank,STATIC,0,true
|
Side Plank,Боковая планка,STATIC,0,true
|
||||||
Sissy Squats,BODYWEIGHT,1.0,false
|
Sissy Squats,Сисси-приседания,BODYWEIGHT,1.0,false
|
||||||
Sprint,CARDIO,0,false
|
Sprint,Спринт,CARDIO,0,false
|
||||||
Squat,STRENGTH,0,false
|
Squat,Приседания со штангой,STRENGTH,0,false
|
||||||
Treadmill,CARDIO,0,false
|
Treadmill,Беговая дорожка,CARDIO,0,false
|
||||||
Tricep Extension,STRENGTH,0,false
|
Tricep Extension,Разгибание рук на трицепс,STRENGTH,0,false
|
||||||
Wall-Sit,STATIC,0,false
|
Wall-Sit,Стульчик у стены,STATIC,0,false
|
||||||
|
|||||||
|
Can't render this file because it contains an unexpected character in line 18 and column 30.
|
Binary file not shown.
@@ -83,6 +83,21 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async initializeAccount(req: any, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.user.userId;
|
||||||
|
const { language } = req.body;
|
||||||
|
if (!language) {
|
||||||
|
return sendError(res, 'Language is required', 400);
|
||||||
|
}
|
||||||
|
const user = await AuthService.initializeUser(userId, language);
|
||||||
|
return sendSuccess(res, { user });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in initializeAccount', { error });
|
||||||
|
return sendError(res, error.message || 'Server error', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async getAllUsers(req: any, res: Response) {
|
static async getAllUsers(req: any, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (req.user.role !== 'ADMIN') {
|
if (req.user.role !== 'ADMIN') {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async function ensureAdminUser() {
|
|||||||
console.info(`ℹ️ Admin user exists but with different email: ${existingAdmin.email}. Expected: ${adminEmail}`);
|
console.info(`ℹ️ Admin user exists but with different email: ${existingAdmin.email}. Expected: ${adminEmail}`);
|
||||||
}
|
}
|
||||||
// Even if admin exists, ensure exercises are seeded (will skip if already has them)
|
// Even if admin exists, ensure exercises are seeded (will skip if already has them)
|
||||||
await AuthService.seedDefaultExercises(existingAdmin.id);
|
await AuthService.seedDefaultExercises(existingAdmin.id, 'en');
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,12 +77,12 @@ async function ensureAdminUser() {
|
|||||||
email: adminEmail,
|
email: adminEmail,
|
||||||
password: hashed,
|
password: hashed,
|
||||||
role: 'ADMIN',
|
role: 'ADMIN',
|
||||||
profile: { create: { weight: 70 } },
|
profile: { create: { weight: 70, language: 'en' } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seed exercises for new admin
|
// Seed exercises for new admin
|
||||||
await AuthService.seedDefaultExercises(admin.id);
|
await AuthService.seedDefaultExercises(admin.id, 'en');
|
||||||
|
|
||||||
console.info(`✅ Admin user created and exercises seeded (email: ${adminEmail})`);
|
console.info(`✅ Admin user created and exercises seeded (email: ${adminEmail})`);
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ router.use(authenticateToken); // Standard middleware now
|
|||||||
router.get('/me', AuthController.getCurrentUser);
|
router.get('/me', AuthController.getCurrentUser);
|
||||||
router.post('/change-password', validate(changePasswordSchema), AuthController.changePassword);
|
router.post('/change-password', validate(changePasswordSchema), AuthController.changePassword);
|
||||||
router.patch('/profile', validate(updateProfileSchema), AuthController.updateProfile);
|
router.patch('/profile', validate(updateProfileSchema), AuthController.updateProfile);
|
||||||
|
router.post('/initialize', AuthController.initializeAccount);
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes
|
||||||
router.get('/users', AuthController.getAllUsers);
|
router.get('/users', AuthController.getAllUsers);
|
||||||
|
|||||||
@@ -67,17 +67,13 @@ export class AuthService {
|
|||||||
include: { profile: true }
|
include: { profile: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seed default exercises
|
|
||||||
// Seed default exercises
|
|
||||||
await this.seedDefaultExercises(user.id);
|
|
||||||
|
|
||||||
const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
|
const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
|
||||||
const { password: _, ...userSafe } = user;
|
const { password: _, ...userSafe } = user;
|
||||||
|
|
||||||
return { user: userSafe, token };
|
return { user: userSafe, token };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async seedDefaultExercises(userId: string) {
|
static async seedDefaultExercises(userId: string, language: string = 'en') {
|
||||||
try {
|
try {
|
||||||
// Ensure env is loaded from root (in case server didn't restart)
|
// Ensure env is loaded from root (in case server didn't restart)
|
||||||
if (!process.env.DEFAULT_EXERCISES_CSV_PATH) {
|
if (!process.env.DEFAULT_EXERCISES_CSV_PATH) {
|
||||||
@@ -110,6 +106,8 @@ export class AuthService {
|
|||||||
const headers = lines[0].split(',').map(h => h.trim());
|
const headers = lines[0].split(',').map(h => h.trim());
|
||||||
const exercisesToCreate = [];
|
const exercisesToCreate = [];
|
||||||
|
|
||||||
|
const nameColumn = language === 'ru' ? 'name_ru' : 'name';
|
||||||
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
const cols = lines[i].split(',').map(c => c.trim());
|
const cols = lines[i].split(',').map(c => c.trim());
|
||||||
if (cols.length < headers.length) continue;
|
if (cols.length < headers.length) continue;
|
||||||
@@ -117,10 +115,12 @@ export class AuthService {
|
|||||||
const row: any = {};
|
const row: any = {};
|
||||||
headers.forEach((h, idx) => row[h] = cols[idx]);
|
headers.forEach((h, idx) => row[h] = cols[idx]);
|
||||||
|
|
||||||
if (row.name && row.type) {
|
const exerciseName = row[nameColumn] || row['name'];
|
||||||
|
|
||||||
|
if (exerciseName && row.type) {
|
||||||
exercisesToCreate.push({
|
exercisesToCreate.push({
|
||||||
userId,
|
userId,
|
||||||
name: row.name,
|
name: exerciseName,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0,
|
bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0,
|
||||||
isUnilateral: row.isUnilateral === 'true',
|
isUnilateral: row.isUnilateral === 'true',
|
||||||
@@ -153,14 +153,34 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async initializeUser(userId: string, language: string) {
|
||||||
|
// Update profile language
|
||||||
|
await prisma.userProfile.upsert({
|
||||||
|
where: { userId },
|
||||||
|
update: { language },
|
||||||
|
create: { userId, language, weight: 70 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed exercises in that language
|
||||||
|
await this.seedDefaultExercises(userId, language);
|
||||||
|
|
||||||
|
// Mark as first login done
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { isFirstLogin: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return updated user
|
||||||
|
return this.getUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
static async changePassword(userId: string, newPassword: string) {
|
static async changePassword(userId: string, newPassword: string) {
|
||||||
const hashed = await bcrypt.hash(newPassword, 10);
|
const hashed = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: {
|
data: {
|
||||||
password: hashed,
|
password: hashed
|
||||||
isFirstLogin: false
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/App.tsx
16
src/App.tsx
@@ -8,6 +8,7 @@ import AICoach from './components/AICoach';
|
|||||||
import Plans from './components/Plans';
|
import Plans from './components/Plans';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import Profile from './components/Profile';
|
import Profile from './components/Profile';
|
||||||
|
import InitializeAccount from './components/InitializeAccount';
|
||||||
import { Language, User } from './types';
|
import { Language, User } from './types';
|
||||||
import { getSystemLanguage } from './services/i18n';
|
import { getSystemLanguage } from './services/i18n';
|
||||||
import { useAuth } from './context/AuthContext';
|
import { useAuth } from './context/AuthContext';
|
||||||
@@ -49,6 +50,10 @@ function App() {
|
|||||||
return <Navigate to="/login" />;
|
return <Navigate to="/login" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentUser?.isFirstLogin && location.pathname !== '/initialize') {
|
||||||
|
return <Navigate to="/initialize" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[100dvh] w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden">
|
<div className="h-[100dvh] w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden">
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
@@ -66,6 +71,17 @@ function App() {
|
|||||||
<Navigate to="/" />
|
<Navigate to="/" />
|
||||||
)
|
)
|
||||||
} />
|
} />
|
||||||
|
<Route path="/initialize" element={
|
||||||
|
currentUser && currentUser.isFirstLogin ? (
|
||||||
|
<InitializeAccount
|
||||||
|
onInitialized={updateUser}
|
||||||
|
language={language}
|
||||||
|
onLanguageChange={setLanguage}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Navigate to="/" />
|
||||||
|
)
|
||||||
|
} />
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<Tracker lang={language} />
|
<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) {
|
if (tempUser && newPassword.length >= 4) {
|
||||||
const res = await changePassword(tempUser.id, newPassword);
|
const res = await changePassword(tempUser.id, newPassword);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
const updatedUser = { ...tempUser, isFirstLogin: false };
|
const updatedUser = { ...tempUser };
|
||||||
onLogin(updatedUser);
|
onLogin(updatedUser);
|
||||||
} else {
|
} else {
|
||||||
setError(res.error || t('change_pass_error', language));
|
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' };
|
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',
|
register_btn: 'Register',
|
||||||
have_account: 'Already have an account? Login',
|
have_account: 'Already have an account? Login',
|
||||||
need_account: 'Need an account? Register',
|
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
|
// General
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
@@ -253,6 +259,12 @@ const translations = {
|
|||||||
register_btn: 'Зарегистрироваться',
|
register_btn: 'Зарегистрироваться',
|
||||||
have_account: 'Уже есть аккаунт? Войти',
|
have_account: 'Уже есть аккаунт? Войти',
|
||||||
need_account: 'Нет аккаунта? Регистрация',
|
need_account: 'Нет аккаунта? Регистрация',
|
||||||
|
init_title: 'Настройка аккаунта',
|
||||||
|
init_desc: 'Добро пожаловать! Выберите предпочтительный язык.',
|
||||||
|
init_select_lang: 'Выберите язык',
|
||||||
|
init_start: 'Начать работу',
|
||||||
|
init_lang_en_desc: 'Интерфейс и названия упражнений по умолчанию будут на английском',
|
||||||
|
init_lang_ru_desc: 'Интерфейс и названия упражнений по умолчанию будут на русском',
|
||||||
|
|
||||||
// General
|
// General
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
|
|||||||
@@ -12,65 +12,61 @@ test.describe('I. Core & Authentication', () => {
|
|||||||
|
|
||||||
// Helper to handle first login if needed
|
// Helper to handle first login if needed
|
||||||
async function handleFirstLogin(page: any) {
|
async function handleFirstLogin(page: any) {
|
||||||
// Wait for either Free Workout (already logged in/not first time)
|
console.log('Starting handleFirstLogin helper...');
|
||||||
// OR Change Password heading
|
const dashboard = page.getByText(/Free Workout|Свободная тренировка/i).first();
|
||||||
// OR Error message
|
const changePass = page.getByRole('heading', { name: /Change Password|Смена пароля/i });
|
||||||
try {
|
const initAcc = page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i });
|
||||||
const heading = page.getByRole('heading', { name: /Change Password/i });
|
|
||||||
const dashboard = page.getByText('Free Workout');
|
|
||||||
const loginButton = page.getByRole('button', { name: 'Login' });
|
|
||||||
|
|
||||||
// Race condition: wait for one of these to appear
|
for (let i = 0; i < 30; i++) {
|
||||||
// We use a small polling or just wait logic.
|
if (await dashboard.isVisible()) {
|
||||||
// Playwright doesn't have "race" for locators easily without Promise.race
|
console.log('Dashboard visible.');
|
||||||
|
|
||||||
// Simple approach: Check if Change Password appears within 5s
|
|
||||||
await expect(heading).toBeVisible({ timeout: 5000 });
|
|
||||||
|
|
||||||
// If we are here, Change Password is visible
|
|
||||||
console.log('Change Password screen detected. Handling...');
|
|
||||||
await page.getByLabel('New Password').fill('StrongNewPass123!');
|
|
||||||
await page.getByRole('button', { name: /Save|Change/i }).click();
|
|
||||||
|
|
||||||
// Now expect dashboard
|
|
||||||
await expect(dashboard).toBeVisible();
|
|
||||||
console.log('Password changed. Dashboard visible.');
|
|
||||||
} catch (e) {
|
|
||||||
// If Change Password didn't appear, maybe we are already at dashboard?
|
|
||||||
if (await page.getByText('Free Workout').isVisible()) {
|
|
||||||
console.log('Already at Dashboard.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check for login error
|
|
||||||
const error = page.locator('.text-error');
|
if (await changePass.isVisible()) {
|
||||||
if (await error.isVisible()) {
|
console.log('Change Password screen detected. Handling...');
|
||||||
console.log('Login Error detected:', await error.textContent());
|
await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
|
||||||
throw new Error(`Login failed: ${await error.textContent()}`);
|
await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
|
||||||
|
// Wait a bit for transition
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// Note: If none of the above, it might be a clean login that just worked fast or failed silently
|
|
||||||
|
if (await initAcc.isVisible()) {
|
||||||
|
console.log('Initialization screen detected. Handling...');
|
||||||
|
await page.getByRole('button', { name: /Get Started|Начать работу/i }).click();
|
||||||
|
// Wait a bit for transition
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final check with assertion to fail the test if not reached
|
||||||
|
await expect(dashboard).toBeVisible({ timeout: 5000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.1. A. Login - Successful Authentication
|
// 1.1. A. Login - Successful Authentication
|
||||||
test('1.1 Login - Successful Authentication', async ({ page, createUniqueUser }) => {
|
test('1.1 Login - Successful Authentication', async ({ page, createUniqueUser }) => {
|
||||||
const user = await createUniqueUser();
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
await page.getByLabel('Email').fill(user.email);
|
await page.getByLabel(/Email/i).fill(user.email);
|
||||||
await page.getByLabel('Password').fill(user.password);
|
await page.getByLabel(/Password|Пароль/i).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: /Login|Войти/i }).click();
|
||||||
|
|
||||||
await handleFirstLogin(page);
|
await handleFirstLogin(page);
|
||||||
|
|
||||||
// Expect redirection to dashboard
|
// Expect redirection to dashboard
|
||||||
await expect(page).not.toHaveURL(/\/login/);
|
await expect(page).not.toHaveURL(/\/login/);
|
||||||
await expect(page.getByText('Free Workout')).toBeVisible();
|
await expect(page.getByText(/Free Workout|Свободная тренировка/i).first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1.2. A. Login - Invalid Credentials
|
// 1.2. A. Login - Invalid Credentials
|
||||||
test('1.2 Login - Invalid Credentials', async ({ page }) => {
|
test('1.2 Login - Invalid Credentials', async ({ page }) => {
|
||||||
await page.getByLabel('Email').fill('invalid@user.com');
|
await page.getByLabel(/Email/i).fill('invalid@user.com');
|
||||||
await page.getByLabel('Password').fill('wrongpassword');
|
await page.getByLabel(/Password|Пароль/i).fill('wrongpassword');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: /Login|Войти/i }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Invalid credentials')).toBeVisible();
|
await expect(page.getByText('Invalid credentials')).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
|
||||||
@@ -79,31 +75,34 @@ test.describe('I. Core & Authentication', () => {
|
|||||||
test('1.3 & 1.4 Login - First-Time Password Change', async ({ page, createUniqueUser }) => {
|
test('1.3 & 1.4 Login - First-Time Password Change', async ({ page, createUniqueUser }) => {
|
||||||
const user = await createUniqueUser();
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
await page.getByLabel('Email').fill(user.email);
|
await page.getByLabel(/Email/i).fill(user.email);
|
||||||
await page.getByLabel('Password').fill(user.password);
|
await page.getByLabel(/Password|Пароль/i).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: /Login|Войти/i }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: /Change Password/i }).first()).toBeVisible({ timeout: 10000 });
|
await expect(page.getByRole('heading', { name: /Change Password|Смена пароля/i }).first()).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// 1.4 Test short password
|
// 1.4 Test short password
|
||||||
await page.getByLabel('New Password').fill('123');
|
await page.getByLabel(/New Password|Новый пароль/i).fill('123');
|
||||||
await page.getByRole('button', { name: /Save|Change/i }).click();
|
await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
|
||||||
await expect(page.getByText('Password too short')).toBeVisible();
|
await expect(page.getByText(/Password too short|Пароль слишком короткий/i)).toBeVisible();
|
||||||
|
|
||||||
// 1.3 Test successful change
|
// 1.3 Test successful change
|
||||||
await page.getByLabel('New Password').fill('StrongNewPass123!');
|
await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
|
||||||
await page.getByRole('button', { name: /Save|Change/i }).click();
|
await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
|
||||||
|
|
||||||
|
// Now we should be on Setup Account page
|
||||||
|
await expect(page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: /Get Started|Начать работу/i }).click();
|
||||||
|
|
||||||
// Now we should be logged in
|
// Now we should be logged in
|
||||||
await expect(page.getByText('Free Workout')).toBeVisible();
|
await expect(page.getByText(/Free Workout|Свободная тренировка/i).first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 1.5. A. Login - Language Selection (English)
|
|
||||||
test('1.5 Login - Language Selection (English)', async ({ page }) => {
|
test('1.5 Login - Language Selection (English)', async ({ page }) => {
|
||||||
await page.getByRole('combobox').selectOption('en');
|
await page.getByRole('combobox').selectOption('en');
|
||||||
await expect(page.getByLabel('Email')).toBeVisible();
|
await expect(page.getByLabel(/Email/i)).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
|
await expect(page.getByRole('button', { name: /Login|Войти/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1.6. A. Login - Language Selection (Russian)
|
// 1.6. A. Login - Language Selection (Russian)
|
||||||
@@ -116,26 +115,26 @@ test.describe('I. Core & Authentication', () => {
|
|||||||
test('1.7 Navigation - Desktop Navigation Rail', async ({ page, createUniqueUser }) => {
|
test('1.7 Navigation - Desktop Navigation Rail', async ({ page, createUniqueUser }) => {
|
||||||
const user = await createUniqueUser();
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
await page.getByLabel('Email').fill(user.email);
|
await page.getByLabel(/Email/i).fill(user.email);
|
||||||
await page.getByLabel('Password').fill(user.password);
|
await page.getByLabel(/Password|Пароль/i).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: /Login|Войти/i }).click();
|
||||||
|
|
||||||
await handleFirstLogin(page);
|
await handleFirstLogin(page);
|
||||||
|
|
||||||
// Set viewport to desktop
|
// Set viewport to desktop
|
||||||
await page.setViewportSize({ width: 1280, height: 720 });
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Tracker' }).first()).toBeVisible();
|
await expect(page.getByRole('button', { name: /Tracker|Трекер/i }).first()).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Plans' }).first()).toBeVisible();
|
await expect(page.getByRole('button', { name: /Plans|Планы/i }).first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1.8. B. Navigation - Mobile Bottom Navigation Bar
|
// 1.8. B. Navigation - Mobile Bottom Navigation Bar
|
||||||
test('1.8 Navigation - Mobile Bottom Navigation Bar', async ({ page, createUniqueUser }) => {
|
test('1.8 Navigation - Mobile Bottom Navigation Bar', async ({ page, createUniqueUser }) => {
|
||||||
const user = await createUniqueUser();
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
await page.getByLabel('Email').fill(user.email);
|
await page.getByLabel(/Email/i).fill(user.email);
|
||||||
await page.getByLabel('Password').fill(user.password);
|
await page.getByLabel(/Password|Пароль/i).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: /Login|Войти/i }).click();
|
||||||
|
|
||||||
await handleFirstLogin(page);
|
await handleFirstLogin(page);
|
||||||
|
|
||||||
@@ -145,7 +144,44 @@ test.describe('I. Core & Authentication', () => {
|
|||||||
await page.waitForTimeout(500); // Allow layout transition
|
await page.waitForTimeout(500); // Allow layout transition
|
||||||
|
|
||||||
// Verify visibility of mobile nav items
|
// Verify visibility of mobile nav items
|
||||||
await expect(page.getByRole('button', { name: 'Tracker' }).last()).toBeVisible();
|
await expect(page.getByRole('button', { name: /Tracker|Трекер/i }).last()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1.9. C. Initialization - Russian Language Seeding
|
||||||
|
test('1.9 Initialization - Russian Language Seeding', async ({ page, createUniqueUser }) => {
|
||||||
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
|
await page.getByLabel(/Email/i).fill(user.email);
|
||||||
|
await page.getByLabel(/Password|Пароль/i).fill(user.password);
|
||||||
|
await page.getByRole('button', { name: /Login|Войти/i }).click();
|
||||||
|
|
||||||
|
// Handle password change
|
||||||
|
await expect(page.getByRole('heading', { name: /Change Password|Смена пароля/i })).toBeVisible();
|
||||||
|
await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
|
||||||
|
await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
|
||||||
|
|
||||||
|
// Handle initialization - Select Russian
|
||||||
|
await expect(page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i })).toBeVisible();
|
||||||
|
await page.getByText('Русский').click();
|
||||||
|
await page.getByRole('button', { name: /Начать работу|Get Started/i }).click();
|
||||||
|
|
||||||
|
// Expect dashboard
|
||||||
|
await expect(page.getByText('Свободная тренировка')).toBeVisible();
|
||||||
|
|
||||||
|
// Verify some exercise is in Russian
|
||||||
|
await page.getByText(/Свободная тренировка|Free Workout/i).first().click();
|
||||||
|
await page.getByLabel(/Выберите упражнение|Select Exercise/i).click();
|
||||||
|
|
||||||
|
// "Air Squats" should be "Приседания" in suggestions
|
||||||
|
await expect(page.getByRole('button', { name: 'Приседания', exact: true })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Приседания', exact: true }).click();
|
||||||
|
|
||||||
|
// Verify it's selected in the input
|
||||||
|
const exerciseInput = page.getByLabel(/Выберите упражнение|Select Exercise/i);
|
||||||
|
await expect(exerciseInput).toHaveValue('Приседания');
|
||||||
|
|
||||||
|
// Verify "Log Set" button is now in Russian
|
||||||
|
await expect(page.getByRole('button', { name: /Записать подход|Log Set/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user