feat: Initial implementation of GymFlow fitness tracking application with workout, plan, and exercise management, stats, and AI coach features.
This commit is contained in:
127
services/auth.ts
Normal file
127
services/auth.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
|
||||
import { User, UserRole, UserProfile } from '../types';
|
||||
import { deleteAllUserData } from './storage';
|
||||
|
||||
const USERS_KEY = 'gymflow_users';
|
||||
|
||||
interface StoredUser extends User {
|
||||
password: string; // In a real app, this would be a hash
|
||||
}
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@gymflow.ai';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
|
||||
export const getUsers = (): StoredUser[] => {
|
||||
try {
|
||||
const data = localStorage.getItem(USERS_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const saveUsers = (users: StoredUser[]) => {
|
||||
localStorage.setItem(USERS_KEY, JSON.stringify(users));
|
||||
};
|
||||
|
||||
export const login = (email: string, password: string): { success: boolean; user?: User; error?: string } => {
|
||||
// 1. Check Admin
|
||||
if (email === ADMIN_EMAIL && password === ADMIN_PASSWORD) {
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
id: 'admin_001',
|
||||
email: ADMIN_EMAIL,
|
||||
role: 'ADMIN',
|
||||
isFirstLogin: false,
|
||||
profile: { weight: 80 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Check Users
|
||||
const users = getUsers();
|
||||
const found = users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
||||
|
||||
if (found && found.password === password) {
|
||||
if (found.isBlocked) {
|
||||
return { success: false, error: 'Account is blocked' };
|
||||
}
|
||||
// Return user without password field
|
||||
const { password, ...userSafe } = found;
|
||||
return { success: true, user: userSafe };
|
||||
}
|
||||
|
||||
return { success: false, error: 'Invalid credentials' };
|
||||
};
|
||||
|
||||
export const createUser = (email: string, password: string): { success: boolean; error?: string } => {
|
||||
const users = getUsers();
|
||||
if (users.find(u => u.email.toLowerCase() === email.toLowerCase())) {
|
||||
return { success: false, error: 'User already exists' };
|
||||
}
|
||||
|
||||
const newUser: StoredUser = {
|
||||
id: crypto.randomUUID(),
|
||||
email,
|
||||
password,
|
||||
role: 'USER',
|
||||
isFirstLogin: true,
|
||||
profile: { weight: 70 }
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
saveUsers(users);
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
export const deleteUser = (userId: string) => {
|
||||
let users = getUsers();
|
||||
users = users.filter(u => u.id !== userId);
|
||||
saveUsers(users);
|
||||
deleteAllUserData(userId);
|
||||
};
|
||||
|
||||
export const toggleBlockUser = (userId: string, block: boolean) => {
|
||||
const users = getUsers();
|
||||
const u = users.find(u => u.id === userId);
|
||||
if (u) {
|
||||
u.isBlocked = block;
|
||||
saveUsers(users);
|
||||
}
|
||||
};
|
||||
|
||||
export const adminResetPassword = (userId: string, newPass: string) => {
|
||||
const users = getUsers();
|
||||
const u = users.find(u => u.id === userId);
|
||||
if (u) {
|
||||
u.password = newPass;
|
||||
u.isFirstLogin = true; // Force them to change it
|
||||
saveUsers(users);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserProfile = (userId: string, profile: Partial<UserProfile>) => {
|
||||
const users = getUsers();
|
||||
const idx = users.findIndex(u => u.id === userId);
|
||||
if (idx >= 0) {
|
||||
users[idx].profile = { ...users[idx].profile, ...profile };
|
||||
saveUsers(users);
|
||||
}
|
||||
};
|
||||
|
||||
export const changePassword = (userId: string, newPassword: string) => {
|
||||
const users = getUsers();
|
||||
const idx = users.findIndex(u => u.id === userId);
|
||||
if (idx >= 0) {
|
||||
users[idx].password = newPassword;
|
||||
users[idx].isFirstLogin = false;
|
||||
saveUsers(users);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCurrentUserProfile = (userId: string): UserProfile | undefined => {
|
||||
if (userId === 'admin_001') return { weight: 80 }; // Mock admin profile
|
||||
const users = getUsers();
|
||||
return users.find(u => u.id === userId)?.profile;
|
||||
}
|
||||
38
services/geminiService.ts
Normal file
38
services/geminiService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { GoogleGenAI, Chat } from "@google/genai";
|
||||
import { WorkoutSession } from '../types';
|
||||
|
||||
const MODEL_ID = 'gemini-2.5-flash';
|
||||
|
||||
export const createFitnessChat = (history: WorkoutSession[]): Chat | null => {
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (!apiKey) return null;
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
|
||||
// Summarize data to reduce token count while keeping relevant context
|
||||
const summary = history.slice(0, 10).map(s => ({
|
||||
date: new Date(s.startTime).toLocaleDateString('ru-RU'),
|
||||
userWeight: s.userBodyWeight,
|
||||
exercises: s.sets.map(set => `${set.exerciseName}: ${set.weight ? set.weight + 'кг' : ''}${set.reps ? ' x ' + set.reps + 'повт' : ''} ${set.distanceMeters ? set.distanceMeters + 'м' : ''}`).join(', ')
|
||||
}));
|
||||
|
||||
const systemInstruction = `
|
||||
Ты — опытный и поддерживающий фитнес-тренер.
|
||||
Твоя задача — анализировать тренировки пользователя и давать краткие, полезные советы на русском языке.
|
||||
|
||||
Учитывай вес пользователя (userWeight в json), если он указан, при анализе прогресса в упражнениях с собственным весом.
|
||||
|
||||
Вот последние 10 тренировок пользователя (в формате JSON):
|
||||
${JSON.stringify(summary)}
|
||||
|
||||
Если пользователь спрашивает о прогрессе, используй эти данные.
|
||||
Отвечай емко, мотивирующе. Избегай длинных лекций, если не просили.
|
||||
`;
|
||||
|
||||
return ai.chats.create({
|
||||
model: MODEL_ID,
|
||||
config: {
|
||||
systemInstruction,
|
||||
},
|
||||
});
|
||||
};
|
||||
279
services/i18n.ts
Normal file
279
services/i18n.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
|
||||
import { Language } from '../types';
|
||||
|
||||
export const getSystemLanguage = (): Language => {
|
||||
if (typeof navigator === 'undefined') return 'en';
|
||||
const lang = navigator.language.split('-')[0];
|
||||
return lang === 'ru' ? 'ru' : 'en';
|
||||
};
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
// Tabs
|
||||
tab_tracker: 'Tracker',
|
||||
tab_plans: 'Plans',
|
||||
tab_history: 'History',
|
||||
tab_stats: 'Stats',
|
||||
tab_ai: 'AI Coach',
|
||||
tab_profile: 'Profile',
|
||||
|
||||
// Auth
|
||||
login_title: 'GymFlow AI',
|
||||
login_email: 'Email',
|
||||
login_password: 'Password',
|
||||
login_btn: 'Login',
|
||||
login_contact_admin: 'Contact administrator to get an account.',
|
||||
login_error: 'Invalid email or password',
|
||||
login_password_short: 'Password too short',
|
||||
change_pass_title: 'Change Password',
|
||||
change_pass_desc: 'This is your first login. Please set a new password.',
|
||||
change_pass_new: 'New Password',
|
||||
change_pass_save: 'Save & Login',
|
||||
|
||||
// Tracker
|
||||
ready_title: 'Ready?',
|
||||
ready_subtitle: 'Start your workout and break records.',
|
||||
my_weight: 'My Weight (kg)',
|
||||
change_in_profile: 'Change in profile',
|
||||
free_workout: 'Free Workout',
|
||||
or_choose_plan: 'Or choose a plan',
|
||||
exercises_count: 'exercises',
|
||||
prep_title: 'Preparation',
|
||||
prep_no_instructions: 'No specific instructions.',
|
||||
cancel: 'Cancel',
|
||||
start: 'Start',
|
||||
finish: 'Finish',
|
||||
plan_completed: 'Plan Completed!',
|
||||
step: 'Step',
|
||||
of: 'of',
|
||||
select_exercise: 'Select Exercise',
|
||||
weight_kg: 'Weight (kg)',
|
||||
reps: 'Reps',
|
||||
time_sec: 'Time (sec)',
|
||||
dist_m: 'Dist (m)',
|
||||
height_cm: 'Height (cm)',
|
||||
body_weight_percent: 'Body Weight',
|
||||
log_set: 'Log Set',
|
||||
prev: 'Prev',
|
||||
history_section: 'History',
|
||||
create_exercise: 'New Exercise',
|
||||
ex_name: 'Name',
|
||||
ex_type: 'Type',
|
||||
create_btn: 'Create',
|
||||
completed_session_sets: 'Completed in this session',
|
||||
add_weight: 'Add. Weight',
|
||||
|
||||
// Types
|
||||
type_strength: 'Strength',
|
||||
type_bodyweight: 'Bodyweight',
|
||||
type_cardio: 'Cardio',
|
||||
type_static: 'Static',
|
||||
type_height: 'Height',
|
||||
type_dist: 'Length',
|
||||
type_jump: 'Plyo',
|
||||
|
||||
// History
|
||||
history_empty: 'History is empty. Finish your first workout!',
|
||||
sets_count: 'Sets',
|
||||
finished: 'Finished',
|
||||
delete_workout: 'Delete workout?',
|
||||
delete_confirm: 'This action cannot be undone.',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
start_time: 'Start',
|
||||
end_time: 'End',
|
||||
max: 'Max',
|
||||
upto: 'Up to',
|
||||
|
||||
// Plans
|
||||
plans_empty: 'No plans created',
|
||||
plan_editor: 'Plan Editor',
|
||||
plan_name_ph: 'E.g. Leg Day',
|
||||
plan_desc_ph: 'Describe preparation...',
|
||||
exercises_list: 'Exercises',
|
||||
weighted: 'Weighted',
|
||||
add_exercise: 'Add Exercise',
|
||||
my_plans: 'My Plans',
|
||||
|
||||
// Stats
|
||||
progress: 'Progress',
|
||||
volume_title: 'Work Volume',
|
||||
volume_subtitle: 'Tonnage (kg * reps)',
|
||||
sets_title: 'Number of Sets',
|
||||
weight_title: 'Body Weight History',
|
||||
not_enough_data: 'Not enough data for statistics. Complete a few workouts!',
|
||||
|
||||
// AI
|
||||
ai_expert: 'AI Expert',
|
||||
ai_intro: 'Hi! I am your AI coach. I analyzed your workouts. Ask me about progress, technique, or routine.',
|
||||
ai_typing: 'Typing...',
|
||||
ai_placeholder: 'Ask about workouts...',
|
||||
ai_error: 'API Key not configured',
|
||||
|
||||
// Profile
|
||||
profile_title: 'Profile',
|
||||
logout: 'Logout',
|
||||
personal_data: 'Personal Data',
|
||||
height: 'Height (cm)',
|
||||
birth_date: 'Birth Date',
|
||||
gender: 'Gender',
|
||||
male: 'Male',
|
||||
female: 'Female',
|
||||
other: 'Other',
|
||||
save_profile: 'Save Profile',
|
||||
change_pass_btn: 'Change Password',
|
||||
admin_area: 'Manage Users',
|
||||
create_user: 'Create User',
|
||||
user_created: 'User created',
|
||||
language: 'Language',
|
||||
admin_users_list: 'Users List',
|
||||
block: 'Block',
|
||||
unblock: 'Unblock',
|
||||
reset_pass: 'Reset Pass',
|
||||
delete_account: 'Delete Account',
|
||||
delete_account_confirm: 'Are you sure? All your data (sessions, plans) will be permanently deleted.',
|
||||
user_deleted: 'User deleted',
|
||||
pass_reset: 'Password reset',
|
||||
manage_exercises: 'Manage Exercises',
|
||||
archive: 'Archive',
|
||||
unarchive: 'Unarchive',
|
||||
show_archived: 'Show Archived',
|
||||
},
|
||||
ru: {
|
||||
// Tabs
|
||||
tab_tracker: 'Трекер',
|
||||
tab_plans: 'Планы',
|
||||
tab_history: 'История',
|
||||
tab_stats: 'Статы',
|
||||
tab_ai: 'AI Тренер',
|
||||
tab_profile: 'Профиль',
|
||||
|
||||
// Auth
|
||||
login_title: 'GymFlow AI',
|
||||
login_email: 'Email',
|
||||
login_password: 'Пароль',
|
||||
login_btn: 'Войти',
|
||||
login_contact_admin: 'Свяжитесь с администратором для получения учетной записи.',
|
||||
login_error: 'Неверный email или пароль',
|
||||
login_password_short: 'Пароль слишком короткий',
|
||||
change_pass_title: 'Смена пароля',
|
||||
change_pass_desc: 'Это ваш первый вход. Пожалуйста, установите новый пароль.',
|
||||
change_pass_new: 'Новый пароль',
|
||||
change_pass_save: 'Сохранить и войти',
|
||||
|
||||
// Tracker
|
||||
ready_title: 'Готовы?',
|
||||
ready_subtitle: 'Начните тренировку и побейте рекорды.',
|
||||
my_weight: 'Мой вес (кг)',
|
||||
change_in_profile: 'Можно изменить в профиле',
|
||||
free_workout: 'Свободная тренировка',
|
||||
or_choose_plan: 'Или выберите план',
|
||||
exercises_count: 'упражнений',
|
||||
prep_title: 'Подготовка',
|
||||
prep_no_instructions: 'Нет особых указаний.',
|
||||
cancel: 'Отмена',
|
||||
start: 'Начать',
|
||||
finish: 'Завершить',
|
||||
plan_completed: 'План выполнен!',
|
||||
step: 'Шаг',
|
||||
of: 'из',
|
||||
select_exercise: 'Выберите упражнение',
|
||||
weight_kg: 'Вес (кг)',
|
||||
reps: 'Повторения',
|
||||
time_sec: 'Время (сек)',
|
||||
dist_m: 'Дистанция (м)',
|
||||
height_cm: 'Высота (см)',
|
||||
body_weight_percent: 'Вес тела',
|
||||
log_set: 'Записать подход',
|
||||
prev: 'Предыдущий',
|
||||
history_section: 'История',
|
||||
create_exercise: 'Новое упражнение',
|
||||
ex_name: 'Название',
|
||||
ex_type: 'Тип упражнения',
|
||||
create_btn: 'Создать',
|
||||
completed_session_sets: 'Выполнено в этой тренировке',
|
||||
add_weight: 'Доп. вес',
|
||||
|
||||
// Types
|
||||
type_strength: 'Силовое',
|
||||
type_bodyweight: 'Свой вес',
|
||||
type_cardio: 'Кардио',
|
||||
type_static: 'Статика',
|
||||
type_height: 'Высота',
|
||||
type_dist: 'Длина',
|
||||
type_jump: 'Прыжки',
|
||||
|
||||
// History
|
||||
history_empty: 'История пуста. Завершите свою первую тренировку!',
|
||||
sets_count: 'Сетов',
|
||||
finished: 'Завершено',
|
||||
delete_workout: 'Удалить тренировку?',
|
||||
delete_confirm: 'Это действие нельзя отменить.',
|
||||
delete: 'Удалить',
|
||||
edit: 'Редактировать',
|
||||
save: 'Сохранить',
|
||||
start_time: 'Начало',
|
||||
end_time: 'Конец',
|
||||
max: 'Макс',
|
||||
upto: 'До',
|
||||
|
||||
// Plans
|
||||
plans_empty: 'Нет созданных планов',
|
||||
plan_editor: 'Редактор плана',
|
||||
plan_name_ph: 'Например: День ног',
|
||||
plan_desc_ph: 'Опишите подготовку...',
|
||||
exercises_list: 'Упражнения',
|
||||
weighted: 'С отягощением',
|
||||
add_exercise: 'Добавить упражнение',
|
||||
my_plans: 'Мои планы',
|
||||
|
||||
// Stats
|
||||
progress: 'Прогресс',
|
||||
volume_title: 'Объем работы',
|
||||
volume_subtitle: 'Тоннаж (кг * повторения)',
|
||||
sets_title: 'Количество сетов',
|
||||
weight_title: 'История веса тела',
|
||||
not_enough_data: 'Недостаточно данных для статистики. Проведите хотя бы пару тренировок!',
|
||||
|
||||
// AI
|
||||
ai_expert: 'AI Эксперт',
|
||||
ai_intro: 'Привет! Я твой AI-тренер. Я проанализировал твои тренировки. Спрашивай меня о прогрессе, технике или плане занятий.',
|
||||
ai_typing: 'Печатает...',
|
||||
ai_placeholder: 'Спроси о тренировках...',
|
||||
ai_error: 'API ключ не настроен',
|
||||
|
||||
// Profile
|
||||
profile_title: 'Профиль',
|
||||
logout: 'Выйти',
|
||||
personal_data: 'Личные данные',
|
||||
height: 'Рост (см)',
|
||||
birth_date: 'Дата рожд.',
|
||||
gender: 'Пол',
|
||||
male: 'Мужской',
|
||||
female: 'Женский',
|
||||
other: 'Другой',
|
||||
save_profile: 'Сохранить профиль',
|
||||
change_pass_btn: 'Сменить пароль',
|
||||
admin_area: 'Управление пользователями',
|
||||
create_user: 'Создать пользователя',
|
||||
user_created: 'Пользователь создан',
|
||||
language: 'Язык / Language',
|
||||
admin_users_list: 'Список пользователей',
|
||||
block: 'Блок',
|
||||
unblock: 'Разблок',
|
||||
reset_pass: 'Сброс пароля',
|
||||
delete_account: 'Удалить аккаунт',
|
||||
delete_account_confirm: 'Вы уверены? Все ваши данные (сессии, планы) будут безвозвратно удалены.',
|
||||
user_deleted: 'Пользователь удален',
|
||||
pass_reset: 'Пароль сброшен',
|
||||
manage_exercises: 'Управление упражнениями',
|
||||
archive: 'Архив',
|
||||
unarchive: 'Вернуть',
|
||||
show_archived: 'Показать архивные',
|
||||
}
|
||||
};
|
||||
|
||||
export const t = (key: keyof typeof translations['en'], lang: Language) => {
|
||||
return translations[lang][key] || translations['en'][key] || key;
|
||||
};
|
||||
137
services/storage.ts
Normal file
137
services/storage.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
import { WorkoutSession, ExerciseDef, ExerciseType, WorkoutSet, WorkoutPlan } from '../types';
|
||||
import { updateUserProfile } from './auth';
|
||||
|
||||
// Helper to namespace keys
|
||||
const getKey = (base: string, userId: string) => `${base}_${userId}`;
|
||||
|
||||
const SESSIONS_KEY = 'gymflow_sessions';
|
||||
const EXERCISES_KEY = 'gymflow_exercises'; // Custom exercises are per user
|
||||
const PLANS_KEY = 'gymflow_plans';
|
||||
|
||||
const DEFAULT_EXERCISES: ExerciseDef[] = [
|
||||
{ id: 'bp', name: 'Жим лежа', type: ExerciseType.STRENGTH },
|
||||
{ id: 'sq', name: 'Приседания со штангой', type: ExerciseType.STRENGTH },
|
||||
{ id: 'dl', name: 'Становая тяга', type: ExerciseType.STRENGTH },
|
||||
{ id: 'pu', name: 'Подтягивания', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 100 },
|
||||
{ id: 'run', name: 'Бег', type: ExerciseType.CARDIO },
|
||||
{ id: 'plank', name: 'Планка', type: ExerciseType.STATIC, bodyWeightPercentage: 100 },
|
||||
{ id: 'dip', name: 'Отжимания на брусьях', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 100 },
|
||||
{ id: 'pushup', name: 'Отжимания от пола', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 65 },
|
||||
{ id: 'air_sq', name: 'Приседания (свой вес)', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 75 },
|
||||
];
|
||||
|
||||
export const getSessions = (userId: string): WorkoutSession[] => {
|
||||
try {
|
||||
const data = localStorage.getItem(getKey(SESSIONS_KEY, userId));
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveSession = (userId: string, session: WorkoutSession): void => {
|
||||
const sessions = getSessions(userId);
|
||||
const index = sessions.findIndex(s => s.id === session.id);
|
||||
if (index >= 0) {
|
||||
sessions[index] = session;
|
||||
} else {
|
||||
sessions.unshift(session);
|
||||
}
|
||||
localStorage.setItem(getKey(SESSIONS_KEY, userId), JSON.stringify(sessions));
|
||||
|
||||
// Auto-update user weight profile if present in session
|
||||
if (session.userBodyWeight) {
|
||||
updateUserProfile(userId, { weight: session.userBodyWeight });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteSession = (userId: string, id: string): void => {
|
||||
let sessions = getSessions(userId);
|
||||
sessions = sessions.filter(s => s.id !== id);
|
||||
localStorage.setItem(getKey(SESSIONS_KEY, userId), JSON.stringify(sessions));
|
||||
};
|
||||
|
||||
export const deleteAllUserData = (userId: string) => {
|
||||
localStorage.removeItem(getKey(SESSIONS_KEY, userId));
|
||||
localStorage.removeItem(getKey(EXERCISES_KEY, userId));
|
||||
localStorage.removeItem(getKey(PLANS_KEY, userId));
|
||||
};
|
||||
|
||||
export const getExercises = (userId: string): ExerciseDef[] => {
|
||||
try {
|
||||
const data = localStorage.getItem(getKey(EXERCISES_KEY, userId));
|
||||
const savedExercises: ExerciseDef[] = data ? JSON.parse(data) : [];
|
||||
|
||||
// Create a map of saved exercises for easy lookup
|
||||
const savedMap = new Map(savedExercises.map(ex => [ex.id, ex]));
|
||||
|
||||
// Start with defaults
|
||||
const mergedExercises = DEFAULT_EXERCISES.map(defEx => {
|
||||
// If user has a saved version of this default exercise (e.g. edited or archived), use that
|
||||
if (savedMap.has(defEx.id)) {
|
||||
const saved = savedMap.get(defEx.id)!;
|
||||
savedMap.delete(defEx.id); // Remove from map so we don't add it again
|
||||
return saved;
|
||||
}
|
||||
return defEx;
|
||||
});
|
||||
|
||||
// Add remaining custom exercises (those that are not overrides of defaults)
|
||||
return [...mergedExercises, ...Array.from(savedMap.values())];
|
||||
} catch (e) {
|
||||
return DEFAULT_EXERCISES;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveExercise = (userId: string, exercise: ExerciseDef): void => {
|
||||
try {
|
||||
const data = localStorage.getItem(getKey(EXERCISES_KEY, userId));
|
||||
let list: ExerciseDef[] = data ? JSON.parse(data) : [];
|
||||
|
||||
const index = list.findIndex(e => e.id === exercise.id);
|
||||
if (index >= 0) {
|
||||
list[index] = exercise;
|
||||
} else {
|
||||
list.push(exercise);
|
||||
}
|
||||
localStorage.setItem(getKey(EXERCISES_KEY, userId), JSON.stringify(list));
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getLastSetForExercise = (userId: string, exerciseId: string): WorkoutSet | undefined => {
|
||||
const sessions = getSessions(userId);
|
||||
for (const session of sessions) {
|
||||
for (let i = session.sets.length - 1; i >= 0; i--) {
|
||||
if (session.sets[i].exerciseId === exerciseId) {
|
||||
return session.sets[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const getPlans = (userId: string): WorkoutPlan[] => {
|
||||
try {
|
||||
const data = localStorage.getItem(getKey(PLANS_KEY, userId));
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const savePlan = (userId: string, plan: WorkoutPlan): void => {
|
||||
const plans = getPlans(userId);
|
||||
const index = plans.findIndex(p => p.id === plan.id);
|
||||
if (index >= 0) {
|
||||
plans[index] = plan;
|
||||
} else {
|
||||
plans.push(plan);
|
||||
}
|
||||
localStorage.setItem(getKey(PLANS_KEY, userId), JSON.stringify(plans));
|
||||
};
|
||||
|
||||
export const deletePlan = (userId: string, id: string): void => {
|
||||
const plans = getPlans(userId).filter(p => p.id !== id);
|
||||
localStorage.setItem(getKey(PLANS_KEY, userId), JSON.stringify(plans));
|
||||
};
|
||||
Reference in New Issue
Block a user