Critical Stability & Performance fixes. Excessive Log Set button gone on QIuck Log screen

This commit is contained in:
AG
2025-12-06 08:58:44 +02:00
parent 27afacee3f
commit 1c3e15516c
35 changed files with 48 additions and 26 deletions

56
src/services/api.ts Normal file
View File

@@ -0,0 +1,56 @@
const API_URL = '/api';
export const getAuthToken = () => localStorage.getItem('token');
export const setAuthToken = (token: string) => localStorage.setItem('token', token);
export const removeAuthToken = () => localStorage.removeItem('token');
const headers = () => {
const token = getAuthToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
};
};
export const api = {
get: async (endpoint: string) => {
const res = await fetch(`${API_URL}${endpoint}`, { headers: headers() });
if (!res.ok) throw new Error(await res.text());
return res.json();
},
post: async (endpoint: string, data: any) => {
const res = await fetch(`${API_URL}${endpoint}`, {
method: 'POST',
headers: headers(),
body: JSON.stringify(data)
});
if (!res.ok) throw new Error(await res.text());
return res.json();
},
put: async (endpoint: string, data: any) => {
const res = await fetch(`${API_URL}${endpoint}`, {
method: 'PUT',
headers: headers(),
body: JSON.stringify(data)
});
if (!res.ok) throw new Error(await res.text());
return res.json();
},
delete: async (endpoint: string) => {
const res = await fetch(`${API_URL}${endpoint}`, {
method: 'DELETE',
headers: headers()
});
if (!res.ok) throw new Error(await res.text());
return res.json();
},
patch: async (endpoint: string, data: any) => {
const res = await fetch(`${API_URL}${endpoint}`, {
method: 'PATCH',
headers: headers(),
body: JSON.stringify(data)
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
};

106
src/services/auth.ts Normal file
View File

@@ -0,0 +1,106 @@
import { User, UserRole, UserProfile } from '../types';
import { api, setAuthToken, removeAuthToken } from './api';
export const getUsers = async (): Promise<{ success: boolean; users?: User[]; error?: string }> => {
try {
const res = await api.get('/auth/users');
return res;
} catch (e) {
return { success: false, error: 'Failed to fetch users' };
}
};
export const login = async (email: string, password: string): Promise<{ success: boolean; user?: User; error?: string }> => {
try {
const res = await api.post('/auth/login', { email, password });
if (res.success) {
setAuthToken(res.token);
return { success: true, user: res.user };
}
return { success: false, error: res.error };
} catch (e: any) {
try {
const err = JSON.parse(e.message);
return { success: false, error: err.error || 'Login failed' };
} catch {
return { success: false, error: 'Login failed' };
}
}
};
export const createUser = async (email: string, password: string): Promise<{ success: boolean; error?: string }> => {
try {
const res = await api.post('/auth/register', { email, password });
if (res.success) {
setAuthToken(res.token);
return { success: true };
}
return { success: false, error: res.error };
} catch (e: any) {
try {
const err = JSON.parse(e.message);
return { success: false, error: err.error || 'Registration failed' };
} catch {
return { success: false, error: 'Registration failed' };
}
}
};
export const deleteUser = async (userId: string) => {
try {
const res = await api.delete(`/auth/users/${userId}`);
return res;
} catch (e) {
return { success: false, error: 'Failed to delete user' };
}
};
export const toggleBlockUser = async (userId: string, block: boolean) => {
try {
const res = await api.patch(`/auth/users/${userId}/block`, { block });
return res;
} catch (e) {
return { success: false, error: 'Failed to update user status' };
}
};
export const adminResetPassword = (userId: string, newPass: string) => {
// Admin only
};
export const updateUserProfile = async (userId: string, profile: Partial<UserProfile>): Promise<{ success: boolean; error?: string }> => {
try {
const res = await api.patch('/auth/profile', { userId, profile });
return res;
} catch (e) {
return { success: false, error: 'Failed to update profile' };
}
};
export const changePassword = async (userId: string, newPassword: string) => {
try {
const res = await api.post('/auth/change-password', { userId, newPassword });
if (!res.success) {
console.error('Failed to change password:', res.error);
}
return res;
} catch (e) {
console.error(e);
return { success: false, error: 'Network error' };
}
};
export const getCurrentUserProfile = (userId: string): UserProfile | undefined => {
// This was synchronous. Now it needs to be async or fetched on load.
// For now, we return undefined and let the app fetch it.
return undefined;
};
export const getMe = async (): Promise<{ success: boolean; user?: User; error?: string }> => {
try {
const res = await api.get('/auth/me');
return res;
} catch (e) {
return { success: false, error: 'Failed to fetch user' };
}
};

View File

@@ -0,0 +1,127 @@
import { WorkoutSession, UserProfile, WorkoutPlan } from '../types';
import { api } from './api';
import { generateId } from '../utils/uuid';
interface FitnessChatOptions {
history: WorkoutSession[];
userProfile?: UserProfile;
plans?: WorkoutPlan[];
lang?: 'en' | 'ru';
sessionId?: string;
}
export const createFitnessChat = (
history: WorkoutSession[],
lang: 'en' | 'ru' = 'ru',
userProfile?: UserProfile,
plans?: WorkoutPlan[]
): any => {
// Generate a unique session ID for this chat instance
const sessionId = generateId();
// Summarize workout history
const workoutSummary = history.slice(0, 20).map(s => ({
date: new Date(s.startTime).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US'),
userWeight: s.userBodyWeight,
planName: s.planName,
exercises: s.sets.map(set => {
const parts = [];
parts.push(set.exerciseName);
if (set.weight) parts.push(`${set.weight}${lang === 'ru' ? 'кг' : 'kg'}`);
if (set.reps) parts.push(`${set.reps}${lang === 'ru' ? 'повт' : 'reps'}`);
if (set.distanceMeters) parts.push(`${set.distanceMeters}${lang === 'ru' ? 'м' : 'm'}`);
if (set.durationSeconds) parts.push(`${set.durationSeconds}${lang === 'ru' ? 'сек' : 's'}`);
return parts.join(' ');
}).join(', ')
}));
// Calculate personal records
const exerciseRecords = new Map<string, { maxWeight?: number; maxReps?: number; maxDistance?: number }>();
history.forEach(session => {
session.sets.forEach(set => {
const current = exerciseRecords.get(set.exerciseName) || {};
exerciseRecords.set(set.exerciseName, {
maxWeight: Math.max(current.maxWeight || 0, set.weight || 0),
maxReps: Math.max(current.maxReps || 0, set.reps || 0),
maxDistance: Math.max(current.maxDistance || 0, set.distanceMeters || 0)
});
});
});
const personalRecords = Array.from(exerciseRecords.entries()).map(([name, records]) => ({
exercise: name,
...records
}));
// Build comprehensive system instruction
const systemInstruction = lang === 'ru' ? `
Ты — опытный и поддерживающий фитнес-тренер AI Coach.
Твоя задача — анализировать тренировки пользователя и давать краткие, полезные советы на русском языке.
ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:
${userProfile ? `
- Вес: ${userProfile.weight || 'не указан'} кг
- Рост: ${userProfile.height || 'не указан'} см
- Пол: ${userProfile.gender || 'не указан'}
` : 'Профиль не заполнен'}
ТРЕНИРОВОЧНЫЕ ПЛАНЫ:
${plans && plans.length > 0 ? JSON.stringify(plans.map(p => ({ name: p.name, exercises: p.steps.map(s => s.exerciseName) }))) : 'Нет активных планов'}
ИСТОРИЯ ТРЕНИРОВОК (последние 20):
${JSON.stringify(workoutSummary, null, 2)}
ЛИЧНЫЕ РЕКОРДЫ:
${JSON.stringify(personalRecords, null, 2)}
ИНСТРУКЦИИ:
- Используй эти данные для анализа прогресса и ответов на вопросы
- Учитывай вес пользователя при анализе упражнений с собственным весом
- Будь конкретным и ссылайся на реальные данные из истории
- Отвечай емко, мотивирующе
- Если данных недостаточно для ответа, честно скажи об этом
` : `
You are an experienced and supportive fitness coach called AI Coach.
Your task is to analyze the user's workouts and provide concise, helpful advice in English.
USER PROFILE:
${userProfile ? `
- Weight: ${userProfile.weight || 'not specified'} kg
- Height: ${userProfile.height || 'not specified'} cm
- Gender: ${userProfile.gender || 'not specified'}
` : 'Profile not filled'}
WORKOUT PLANS:
${plans && plans.length > 0 ? JSON.stringify(plans.map(p => ({ name: p.name, exercises: p.steps.map(s => s.exerciseName) }))) : 'No active plans'}
WORKOUT HISTORY (last 20 sessions):
${JSON.stringify(workoutSummary, null, 2)}
PERSONAL RECORDS:
${JSON.stringify(personalRecords, null, 2)}
INSTRUCTIONS:
- Use this data to analyze progress and answer questions
- Consider user's weight when analyzing bodyweight exercises
- Be specific and reference actual data from the history
- Answer concisely and motivationally
- If there's insufficient data to answer, be honest about it
- ALWAYS answer in the language the user speaks to you
`;
return {
sendMessage: async (userMessage: string) => {
const res = await api.post('/ai/chat', {
systemInstruction,
userMessage,
sessionId
});
return {
text: res.response,
response: {
text: () => res.response
}
};
}
};
};

352
src/services/i18n.ts Normal file
View File

@@ -0,0 +1,352 @@
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',
change_pass_error: 'Error changing password',
passwords_mismatch: 'Passwords do not match',
register_title: 'Create Account',
confirm_password: 'Confirm Password',
register_btn: 'Register',
have_account: 'Already have an account? Login',
need_account: 'Need an account? Register',
// 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',
quit: 'Quit',
start: 'Start',
finish: 'Finish',
finish_confirm_title: 'Finish Workout?',
finish_confirm_msg: 'Are you sure you want to finish this workout? Your progress will be saved.',
quit_no_save: 'Quit without saving',
quit_confirm_title: 'Quit without saving?',
quit_confirm_msg: 'All progress from this session will be lost. This action cannot be undone.',
confirm: 'Confirm',
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',
no_exercises_found: 'No exercises found',
// Types
type_strength: 'Free Weights & Machines',
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_set: 'Delete set?',
delete_confirm: 'This action cannot be undone.',
delete_set_confirm: 'Are you sure you want to delete this set?',
delete: 'Delete',
edit: 'Edit',
save: 'Save',
start_time: 'Start',
end_time: 'End',
max: 'Max',
upto: 'Up to',
no_plan: 'No plan',
// Plans
plans_empty: 'No plans created',
plan_editor: 'Plan Editor',
plan_name_ph: 'E.g. Full-body Routine',
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 Coach',
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',
filter_by_name: 'Filter by name',
type_to_filter: 'Type to filter...',
exercise_name_exists: 'An exercise with this name already exists',
profile_saved: 'Profile saved successfully',
// Sporadic Sets
quick_log: 'Quick Log',
sporadic_sets_title: 'Quick Logged Sets',
log_sporadic_success: 'Set logged successfully',
sporadic_set_note: 'Note (optional)',
done: 'Done',
saved: 'Saved',
// Unilateral exercises
unilateral_exercise: 'Unilateral exercise (separate left/right tracking)',
unilateral: 'Unilateral',
same_values_both_sides: 'Same values for both sides',
left: 'Left',
right: 'Right',
},
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: 'Сохранить и войти',
change_pass_error: 'Ошибка смены пароля',
passwords_mismatch: 'Пароли не совпадают',
register_title: 'Регистрация',
confirm_password: 'Подтвердите пароль',
register_btn: 'Зарегистрироваться',
have_account: 'Уже есть аккаунт? Войти',
need_account: 'Нет аккаунта? Регистрация',
// 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: 'Завершить',
finish_confirm_title: 'Завершить тренировку?',
finish_confirm_msg: 'Вы уверены, что хотите завершить эту тренировку? Ваш прогресс будет сохранен.',
quit_no_save: 'Выйти без сохранения',
quit_confirm_title: 'Выйти без сохранения?',
quit_confirm_msg: 'Весь прогресс этой сессии будет потерян. Это действие нельзя отменить.',
confirm: 'Подтвердить',
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: 'Доп. вес',
no_exercises_found: 'Упражнения не найдены',
// Types
type_strength: 'Свободные веса и тренажеры',
type_bodyweight: 'Свой вес',
type_cardio: 'Кардио',
type_static: 'Статика',
type_height: 'Высота',
type_dist: 'Длина',
type_jump: 'Прыжки',
// History
history_empty: 'История пуста. Завершите свою первую тренировку!',
sets_count: 'Сетов',
finished: 'Завершено',
delete_workout: 'Удалить тренировку?',
delete_set: 'Удалить подход?',
delete_confirm: 'Это действие нельзя отменить.',
delete_set_confirm: 'Вы уверены, что хотите удалить этот подход?',
delete: 'Удалить',
edit: 'Редактировать',
save: 'Сохранить',
start_time: 'Начало',
end_time: 'Конец',
max: 'Макс',
upto: 'До',
no_plan: 'Без плана',
// 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: 'Показать архивные',
filter_by_name: 'Фильтр по названию',
type_to_filter: 'Введите для фильтрации...',
exercise_name_exists: 'Упражнение с таким названием уже существует',
profile_saved: 'Профиль успешно сохранен',
// Sporadic Sets
quick_log: 'Быстрая запись',
sporadic_sets_title: 'Быстрые записи',
log_sporadic_success: 'Сет записан',
sporadic_set_note: 'Заметка (опц.)',
done: 'Готово',
saved: 'Сохранено',
// Unilateral exercises
unilateral_exercise: 'Односторонее упражнение (отдельно левая/правая)',
unilateral: 'Одностороннее',
same_values_both_sides: 'Одинаковые значения для обеих сторон',
left: 'Левая',
right: 'Правая',
},
};
export const t = (key: keyof typeof translations['en'], lang: Language) => {
return translations[lang][key] || translations['en'][key] || key;
};

View File

@@ -0,0 +1,69 @@
import { api } from './api';
import { SporadicSet } from '../types';
export async function getSporadicSets(): Promise<SporadicSet[]> {
try {
const response = await api.get('/sporadic-sets');
if (response.success) {
return response.sporadicSets || [];
}
return [];
} catch (error) {
console.error('Failed to fetch sporadic sets:', error);
return [];
}
}
export async function logSporadicSet(setData: {
exerciseId: string;
weight?: number;
reps?: number;
durationSeconds?: number;
distanceMeters?: number;
height?: number;
bodyWeightPercentage?: number;
note?: string;
side?: 'LEFT' | 'RIGHT';
}): Promise<SporadicSet | null> {
try {
const response = await api.post('/sporadic-sets', setData);
if (response.success) {
return response.sporadicSet;
}
return null;
} catch (error) {
console.error('Failed to log sporadic set:', error);
return null;
}
}
export async function updateSporadicSet(id: string, setData: {
weight?: number;
reps?: number;
durationSeconds?: number;
distanceMeters?: number;
height?: number;
bodyWeightPercentage?: number;
note?: string;
}): Promise<SporadicSet | null> {
try {
const response = await api.put(`/sporadic-sets/${id}`, setData);
if (response.success) {
return response.sporadicSet;
}
return null;
} catch (error) {
console.error('Failed to update sporadic set:', error);
return null;
}
}
export async function deleteSporadicSet(id: string): Promise<boolean> {
try {
const response = await api.delete(`/sporadic-sets/${id}`);
return response.success || false;
} catch (error) {
console.error('Failed to delete sporadic set:', error);
return false;
}
}

114
src/services/storage.ts Normal file
View File

@@ -0,0 +1,114 @@
import { WorkoutSession, ExerciseDef, ExerciseType, WorkoutSet, WorkoutPlan } from '../types';
import { api } from './api';
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
try {
const sessions = await api.get('/sessions');
// Convert ISO date strings to timestamps
return sessions.map((session: any) => ({
...session,
startTime: new Date(session.startTime).getTime(),
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
sets: session.sets.map((set: any) => ({
...set,
exerciseName: set.exercise?.name || 'Unknown',
type: set.exercise?.type || 'STRENGTH'
}))
}));
} catch {
return [];
}
};
export const saveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
await api.post('/sessions', session);
};
export const getActiveSession = async (userId: string): Promise<WorkoutSession | null> => {
try {
const response = await api.get('/sessions/active');
if (!response.success || !response.session) {
return null;
}
const session = response.session;
// Convert ISO date strings to timestamps
return {
...session,
startTime: new Date(session.startTime).getTime(),
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
sets: session.sets.map((set: any) => ({
...set,
exerciseName: set.exercise?.name || 'Unknown',
type: set.exercise?.type || 'STRENGTH'
}))
};
} catch {
return null;
}
};
export const updateActiveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
await api.put('/sessions/active', session);
};
export const deleteSetFromActiveSession = async (userId: string, setId: string): Promise<void> => {
await api.delete(`/sessions/active/set/${setId}`);
};
export const updateSetInActiveSession = async (userId: string, setId: string, setData: Partial<WorkoutSet>): Promise<WorkoutSet> => {
const response = await api.put(`/sessions/active/set/${setId}`, setData);
return response.updatedSet;
};
export const deleteActiveSession = async (userId: string): Promise<void> => {
await api.delete('/sessions/active');
};
export const deleteSession = async (userId: string, id: string): Promise<void> => {
await api.delete(`/sessions/${id}`);
};
export const deleteAllUserData = (userId: string) => {
// Not implemented in frontend
};
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
try {
return await api.get('/exercises');
} catch {
return [];
}
};
export const saveExercise = async (userId: string, exercise: ExerciseDef): Promise<void> => {
await api.post('/exercises', exercise);
};
export const getLastSetForExercise = async (userId: string, exerciseId: string): Promise<WorkoutSet | undefined> => {
try {
const response = await api.get(`/exercises/${exerciseId}/last-set`);
if (response.success && response.set) {
return response.set;
}
return undefined;
} catch (error) {
console.error("Failed to fetch last set:", error);
return undefined;
}
};
export const getPlans = async (userId: string): Promise<WorkoutPlan[]> => {
try {
return await api.get('/plans');
} catch {
return [];
}
};
export const savePlan = async (userId: string, plan: WorkoutPlan): Promise<void> => {
await api.post('/plans', plan);
};
export const deletePlan = async (userId: string, id: string): Promise<void> => {
await api.delete(`/plans/${id}`);
};

53
src/services/weight.ts Normal file
View File

@@ -0,0 +1,53 @@
import { BodyWeightRecord } from '../types';
const API_URL = '/api';
export const getWeightHistory = async (): Promise<BodyWeightRecord[]> => {
const token = localStorage.getItem('token');
if (!token) return [];
try {
const response = await fetch(`${API_URL}/weight`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch weight history');
}
return await response.json();
} catch (error) {
console.error('Error fetching weight history:', error);
return [];
}
};
export const logWeight = async (weight: number, dateStr?: string): Promise<BodyWeightRecord | null> => {
const token = localStorage.getItem('token');
if (!token) return null;
try {
// Default to today if no date provided
const date = dateStr || new Date().toISOString().split('T')[0];
const response = await fetch(`${API_URL}/weight`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ weight, dateStr: date })
});
if (!response.ok) {
throw new Error('Failed to log weight');
}
return await response.json();
} catch (error) {
console.error('Error logging weight:', error);
return null;
}
};