From 4814d2d310eaa40192c0f2cdda99236ff391e945 Mon Sep 17 00:00:00 2001 From: Tatyana Date: Mon, 25 Aug 2025 16:13:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D1=8B?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=D0=B0=20=D1=81=20?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D1=81=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B6=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20-=20?= =?UTF-8?q?=D1=81=20=D1=83=D0=BF=D1=80=D0=B0=D0=B6=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BD=D0=B0=201=20=D1=83=D0=BF=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AppRoutes.tsx | 49 ++-- src/confconnect.js | 57 ++-- src/pages/CourseExercises.tsx | 459 +++++++++++++++++++++++++----- src/pages/Exercise old.tsx | 240 ++++++++++++++++ src/pages/Exercise.tsx | 250 ++++++++++++---- src/shared/consts/localStorage.ts | 4 +- src/shared/consts/router.ts | 27 +- 7 files changed, 886 insertions(+), 200 deletions(-) create mode 100644 src/pages/Exercise old.tsx diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index ec8b644..adde800 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -1,24 +1,23 @@ -import { Switch, Route, Redirect } from "react-router-dom"; +import { Switch, Route, Redirect } from "react-router-dom" -import Welcome from "./pages/Welcome"; -import Login from "./pages/Login"; -import Home from "./pages/Home"; -import ForgotPasword from "./pages/ForgotPassword"; -import { Courses } from "./pages/Courses"; -import CourseExercises from "./pages/CourseExercises"; +import Welcome from "./pages/Welcome" +import Login from "./pages/Login" +import Home from "./pages/Home" +import ForgotPasword from "./pages/ForgotPassword" +import { Courses } from "./pages/Courses" +import CourseExercises from "./pages/CourseExercises" +import { Exercise } from "./pages/Exercise" -import Settings from "./pages/Settings"; -import CourseComplete from "./pages/CourseComplete"; +import Settings from "./pages/Settings" +import CourseComplete from "./pages/CourseComplete" -import { getRouteWelcome } from "./shared/consts/router"; -import { getRouteLogin } from "./shared/consts/router"; -import { getRouteHome } from "./shared/consts/router"; -import { getRouteForgotPassword } from "./shared/consts/router"; -import { getRouteCourses } from "./shared/consts/router"; - - -import { getRouteSettings } from "./shared/consts/router"; -import { getRouteCourseComplete } from "./shared/consts/router"; +import { getRouteWelcome } from "./shared/consts/router" +import { getRouteLogin } from "./shared/consts/router" +import { getRouteHome } from "./shared/consts/router" +import { getRouteForgotPassword } from "./shared/consts/router" +import { getRouteCourses } from "./shared/consts/router" +import { getRouteSettings } from "./shared/consts/router" +import { getRouteCourseComplete } from "./shared/consts/router" const AppRoutes = () => ( @@ -30,16 +29,12 @@ const AppRoutes = () => ( - - - + + + - - - - -); +) -export default AppRoutes; \ No newline at end of file +export default AppRoutes diff --git a/src/confconnect.js b/src/confconnect.js index 9962955..d872a46 100644 --- a/src/confconnect.js +++ b/src/confconnect.js @@ -1,41 +1,36 @@ -import axios from 'axios'; - -import { AUTH_TOKEN, USER_KEY } from '@/shared/consts/localStorage'; -import { getRouteLogin } from '@/shared/consts/router'; +import axios from "axios" const instance = axios.create({ - baseURL: 'http://localhost:8093/', - headers: { - 'Content-type': 'application/json', - }, -}); + baseURL: "http://localhost:8093/", + headers: { + "Content-type": "application/json", + }, +}) instance.interceptors.response.use(undefined, async (error) => { - if (error.response?.status === 401) { - localStorage.removeItem(AUTH_TOKEN); - localStorage.removeItem(USER_KEY); + if (error.response?.status === 401) { + localStorage.removeItem("authToken") + localStorage.removeItem("token") + localStorage.removeItem("userId") + localStorage.removeItem("userName") - // перехватывает запросы и брал пути из функции + window.location.href = "/login" + } - window.location.href = getRouteLogin(); - } - - return Promise.reject(error); -}); + return Promise.reject(error) +}) instance.interceptors.request.use( - (config) => { - const token = localStorage.getItem(AUTH_TOKEN); - const c = config; - - if (token && config) { - c.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error), -); - -export { instance as connect }; + (config) => { + const token = localStorage.getItem("authToken") || localStorage.getItem("token") + const c = config + if (token && config) { + c.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => Promise.reject(error), +) +export { instance as connect } diff --git a/src/pages/CourseExercises.tsx b/src/pages/CourseExercises.tsx index 65c1a57..8e2ceda 100644 --- a/src/pages/CourseExercises.tsx +++ b/src/pages/CourseExercises.tsx @@ -3,104 +3,423 @@ import type React from "react" import { useState, useEffect } from "react" import { useHistory, useParams } from "react-router-dom" - import HeaderNav from "../components/HeaderNav" import BottomNavigation from "../components/BottomNavigation" +import { connect } from "../confconnect" +import { getRouteExercise } from "../shared/consts/router" import video from "../assets/img/video.mov" -import { connect } from '../confconnect'; -import { getRouteExercise } from "../shared/consts/router" -import type { Exercise } from "./Exercise" - - interface RouteParams { - id: string; + id: string } -const CourseExercises: React.FC =() => { - const history = useHistory() - const { id } = useParams(); // получаем ID курса из URL - const [currentSlide, setCurrentSlide] = useState(0); - const token = localStorage.getItem('authToken'); +interface Exercise { + id: number + title: string + desc: string + url_file_img: string + url_file: string + time: string + repeats: number + count: number + position: number + day: number + sessionname: string +} +interface Course { + ID: number + title: string + desc: string + url_file_img: string +} + +interface ExercisesByDay { + [day: number]: Exercise[] +} + +const CourseExercises: React.FC = () => { + const history = useHistory() + const { id } = useParams() + + const [exercises, setExercises] = useState([]) + const [exercisesByDay, setExercisesByDay] = useState({}) + const [course, setCourse] = useState(null) + const [error, setError] = useState("") + const [loading, setLoading] = useState(true) + const [currentSlide, setCurrentSlide] = useState(0) + const [selectedDay, setSelectedDay] = useState(1) useEffect(() => { - console.log(token) + console.log("Course ID:", id) + + // Проверяем токен перед запросом + const token = localStorage.getItem("authToken") || localStorage.getItem("token") + console.log("Token exists:", !!token) + if (!token) { - setError('Токен не найден'); - return; + console.log("No token found, redirecting to login") + history.push("/login") + return } - connect.get(`/pacient/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .then(response => { - console.log('Response status:', response.status); - - // Предполагаемая структура: - // response.data.courses — массив пользователей - const exercises = response.data.exercises || []; + if (!id) { + setError("ID курса не найден") + setLoading(false) + return + } - // Собираем все курсы из всех пользователей - const allCourse: Exercises[] = []; + // Получаем все упражнения курса + const fetchAllExercises = async () => { + try { + const allExercises: Exercise[] = [] + let exerciseId = 1 + let hasMoreExercises = true + let consecutiveErrors = 0 - users.forEach(user => { - if (user.Courses && Array.isArray(user.Courses)) { - user.Courses.forEach(course => { - // Можно добавить проверку или преобразование - allCourses.push({ - ID: course.id, // или course.ID, зависит от структуры - title: course.title, - desc: course.desc, - url_file_img: course.url_file_img, - }); - }); + // Получаем упражнения по одному, пока не закончатся + while (hasMoreExercises && exerciseId <= 20 && consecutiveErrors < 3) { + try { + console.log(`Fetching exercise ${exerciseId} for course ${id}`) + const response = await connect.get(`pacient/${id}/${exerciseId}`) + console.log(`Exercise ${exerciseId} response:`, response.data) + + if (response.data && response.data.id) { + allExercises.push(response.data) + exerciseId++ + consecutiveErrors = 0 // Сбрасываем счетчик ошибок + } else { + hasMoreExercises = false + } + } catch (error: any) { + console.log(`Error fetching exercise ${exerciseId}:`, error.response?.status) + if (error.response?.status === 404) { + consecutiveErrors++ + exerciseId++ + } else if (error.response?.status === 401) { + // Токен недействителен + localStorage.removeItem("authToken") + localStorage.removeItem("token") + history.push("/login") + return + } else { + consecutiveErrors++ + exerciseId++ + } + } } - }); - setCourses(allCourses); - }) - .catch(error => { - if (error.response) { - console.error('Ошибка ответа сервера:', error.response.status, error.response.data); - setError(`Ошибка сервера: ${error.response.status}`); - } else if (error.request) { - console.error('Нет ответа от сервера:', error.request); - setError('Нет ответа от сервера'); - } else { - console.error('Ошибка при настройке запроса:', error.message); - setError(`Ошибка: ${error.message}`); + console.log("All exercises fetched:", allExercises) + + if (allExercises.length > 0) { + // Сортируем упражнения по дням и позиции + const sortedExercises = allExercises.sort((a, b) => { + if (a.day !== b.day) { + return a.day - b.day + } + return a.position - b.position + }) + + // Группируем упражнения по дням + const groupedByDay: ExercisesByDay = {} + sortedExercises.forEach((exercise) => { + if (!groupedByDay[exercise.day]) { + groupedByDay[exercise.day] = [] + } + groupedByDay[exercise.day].push(exercise) + }) + + console.log("Exercises grouped by day:", groupedByDay) + + setExercises(sortedExercises) + setExercisesByDay(groupedByDay) + + // Устанавливаем первый доступный день + const firstDay = Math.min(...Object.keys(groupedByDay).map(Number)) + setSelectedDay(firstDay) + + // Устанавливаем информацию о курсе + setCourse({ + ID: Number.parseInt(id), + title: `Курс ${id}`, + desc: "Упражнения для реабилитации", + url_file_img: "", + }) + } else { + setError("Упражнения не найдены") + } + + setLoading(false) + } catch (error: any) { + console.error("Ошибка при получении упражнений:", error) + + if (error.response?.status === 401) { + localStorage.removeItem("authToken") + localStorage.removeItem("token") + history.push("/login") + return + } + + if (error.response) { + console.error("Ошибка ответа сервера:", error.response.status, error.response.data) + setError(`Ошибка сервера: ${error.response.status}`) + } else if (error.request) { + console.error("Нет ответа от сервера:", error.request) + setError("Нет ответа от сервера") + } else { + console.error("Ошибка при настройке запроса:", error.message) + setError(`Ошибка: ${error.message}`) + } + + setLoading(false) } - }); - }, [token]); + } + fetchAllExercises() + }, [id, history]) + // Получаем упражнения для выбранного дня + const currentDayExercises = exercisesByDay[selectedDay] || [] + const currentExercise = currentDayExercises[currentSlide] || exercises[0] + // Функции для переключения на следующее/предыдущее упражнение + const nextExercise = () => { + if (currentDayExercises.length > 0) { + setCurrentSlide((prev) => (prev + 1) % currentDayExercises.length) + } + } + const prevExercise = () => { + if (currentDayExercises.length > 0) { + setCurrentSlide((prev) => (prev - 1 + currentDayExercises.length) % currentDayExercises.length) + } + } + const getDifficultyColor = (difficulty: string) => { + switch (difficulty) { + case "Легкий": + return "bg-gradient-to-r from-emerald-400 to-green-500" + case "Средний": + return "bg-gradient-to-r from-amber-400 to-orange-500" + case "Сложный": + return "bg-gradient-to-r from-red-400 to-pink-500" + default: + return "bg-gradient-to-r from-cyan-400 to-cyan-600" + } + } + if (loading) { + return ( +
+
+
+
+

Загрузка упражнений...

+
+
+
+ ) + } + + if (error) { + return ( +
+
+ +
+
+

{error}

+ +
+
+
+
+ ) + } + + if (!course || exercises.length === 0) { + return ( +
+
+ +
+
+

В этом курсе пока нет упражнений

+ +
+
+
+
+ ) + } + + const availableDays = Object.keys(exercisesByDay) + .map(Number) + .sort((a, b) => a - b) return ( - - -
+
- -
-

Курс ID: {id}

+ -

+ {/* Day Selector */} + {availableDays.length > 1 && ( +
+
+ {availableDays.map((day) => ( + + ))} +
+
+ )} - {/* тут можете использовать id для загрузки данных или другого */} + {/* Exercise Slider */} + {currentDayExercises.length > 0 && currentExercise && ( +
+
+ {/* Exercise Image */} +
+ {currentExercise.title} +
+ + {/* Time Badge */} +
+
+ {currentExercise.time} мин +
+
+ + {/* Navigation arrows */} + {currentDayExercises.length > 1 && ( + <> + + + + )} +
+ + {/* Exercise Info */} +
+

{currentExercise.title}

+

{currentExercise.desc}

+ + +
+
+ + {/* Slide indicators */} + {currentDayExercises.length > 1 && ( +
+ {currentDayExercises.map((_, index) => ( +
+ )} +
+ )} + + {/* Exercise List for Selected Day */} +
+

+ {availableDays.length > 1 ? `Упражнения на день ${selectedDay}` : "Все упражнения курса"} +

+
+ {currentDayExercises.map((exercise, index) => ( +
history.push(getRouteExercise(id!, exercise.id.toString()))} + className={`glass-morphism rounded-2xl p-4 sm:p-6 border border-white/50 shadow-lg cursor-pointer transition-all duration-300 hover:shadow-2xl transform hover:scale-[1.02] backdrop-blur-xl ${ + index === currentSlide ? "ring-2 ring-[#2BACBE] bg-cyan-50/20" : "" + }`} + > +
+
+
+ {exercise.position} +
+
+
+

{exercise.title}

+

{exercise.desc}

+
+ {exercise.time} мин + + {exercise.count} подходов + + {exercise.repeats} повторений +
+
+
+ + + +
+
+
+ ))} +
+
+ + +
+ ) +} - -
-
- ); -}; - - -export default CourseExercises; \ No newline at end of file +export default CourseExercises diff --git a/src/pages/Exercise old.tsx b/src/pages/Exercise old.tsx new file mode 100644 index 0000000..b43f781 --- /dev/null +++ b/src/pages/Exercise old.tsx @@ -0,0 +1,240 @@ +"use client" + +import { useState, useEffect } from "react" +import { useHistory } from "react-router-dom" + +import BottomNavigation from "../components/BottomNavigation" +import { getRouteCourseComplete } from "../shared/consts/router" +import HeaderNav from "../components/HeaderNav" +import video from "../assets/img/video.mov" + + +export interface Exercise { + id: number; + title: string; + desc: string; + url_file: string; + url_file_img: string; +} + + + + + +export const Exercise = () => { + const history = useHistory() + // const { id } = useParams<{ id: string }>() + const [isPlaying, setIsPlaying] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [totalTime] = useState(900) // 15 minutes in seconds + const [currentSet, setCurrentSet] = useState(1) + const [totalSets] = useState(3) + + useEffect(() => { + let interval: NodeJS.Timeout | undefined + if (isPlaying) { + interval = setInterval(() => { + setCurrentTime((prev) => { + if (prev >= totalTime) { + setIsPlaying(false) + // Show completion animation + history.push(getRouteCourseComplete()) + return totalTime + } + return prev + 1 + }) + }, 1000) + } + return () => { + if (interval) clearInterval(interval) + } + }, [isPlaying, totalTime, history]) + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}` + } + + const progress = (currentTime / totalTime) * 100 + + + + const PlayIcon = () => ( + + + + ) + + const PauseIcon = () => ( + + + + ) + + const RefreshIcon = () => ( + + ) + + const CheckIcon = () => ( + + + + ) + + const exerciseSteps = [ + { + title: "Исходное положение", + description: + "Лягте на спирку на коврик. Руки вдоль тела, ладони прижаты к полу. Ноги выпрямлены, носки направлены вверх. Поясница плотно прижата к полу.", + }, + { + title: "Задание", + description: + "Медленно поднимите прямые ноги до угла 90 градусов. Задержитесь на 2 секунды, затем медленно опустите ноги, не касаясь пола. Повторите движение плавно и контролируемо.", + }, + { + title: "Подходы", + description: "Выполните 3 подхода по 12 повторений с отдыхом 60 секунд между подходами.", + }, + { + title: "Перерыв", + description: "Отдыхайте 60 секунд между подходами. Дышите спокойно и расслабьте мышцы.", + }, + { + title: "Динамика и статика", + description: + "Динамическая фаза: подъем и опускание ног выполняется плавно, 2 секунды вверх, 2 секунды вниз. Статическая фаза: удержание ног в верхней точке на 2 секунды.", + }, + ] + + + return ( + +
+
+ + + + +
+
+
+
+
+
+ +
+ {exerciseSteps.map((step, index) => ( +
+
+ +
+

{step.title}

+

{step.description}

+
+
+
+ ))} +
+ + {/* Fixed Timer at Bottom */} +
+
+
+
+
+ + Подход {currentSet} из {totalSets} + +
+ + {formatTime(currentTime)} / {formatTime(totalTime)} + +
+
+
+
+
+ + + +
+
+
+ +
+
+ ) +} + + diff --git a/src/pages/Exercise.tsx b/src/pages/Exercise.tsx index b43f781..5c1d9b0 100644 --- a/src/pages/Exercise.tsx +++ b/src/pages/Exercise.tsx @@ -1,34 +1,105 @@ "use client" import { useState, useEffect } from "react" -import { useHistory } from "react-router-dom" +import { useHistory, useParams } from "react-router-dom" import BottomNavigation from "../components/BottomNavigation" import { getRouteCourseComplete } from "../shared/consts/router" import HeaderNav from "../components/HeaderNav" import video from "../assets/img/video.mov" - +import { connect } from "../confconnect" export interface Exercise { - id: number; - title: string; - desc: string; - url_file: string; - url_file_img: string; + id: number + title: string + desc: string + url_file: string + url_file_img: string + time: string + repeats: number + count: number + position: number + day: number + sessionname: string } - - - +interface RouteParams { + courseId: string + exerciseId: string +} export const Exercise = () => { const history = useHistory() - // const { id } = useParams<{ id: string }>() + const { courseId, exerciseId } = useParams() + const [exercise, setExercise] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState("") const [isPlaying, setIsPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) - const [totalTime] = useState(900) // 15 minutes in seconds + const [totalTime, setTotalTime] = useState(900) // Default 15 minutes const [currentSet, setCurrentSet] = useState(1) - const [totalSets] = useState(3) + const [totalSets, setTotalSets] = useState(3) + + useEffect(() => { + console.log("Course ID:", courseId) + console.log("Exercise ID:", exerciseId) + + if (!courseId || !exerciseId) { + setError("ID курса или упражнения не найден") + setLoading(false) + return + } + + // Получаем данные упражнения через API: GET /pacient/:course_id/:exercise_id + connect + .get(`pacient/${courseId}/${exerciseId}`) + .then((response) => { + console.log("Response status:", response.status) + console.log("Response data:", response.data) + + const exerciseData = response.data + + setExercise({ + id: exerciseData.id, + title: exerciseData.title, + desc: exerciseData.desc, + url_file: exerciseData.url_file, + url_file_img: exerciseData.url_file_img, + time: exerciseData.time, + repeats: exerciseData.repeats, + count: exerciseData.count, + position: exerciseData.position, + day: exerciseData.day, + sessionname: exerciseData.sessionname, + }) + + // Устанавливаем время и подходы из данных упражнения + if (exerciseData.time) { + setTotalTime(Number.parseInt(exerciseData.time) * 60) // Конвертируем минуты в секунды + } + if (exerciseData.count) { + setTotalSets(exerciseData.count) + } + + setLoading(false) + }) + .catch((error) => { + console.error("Ошибка при получении упражнения:", error) + + if (error.response) { + console.error("Ошибка ответа сервера:", error.response.status, error.response.data) + setError(`Ошибка сервера: ${error.response.status}`) + } else if (error.request) { + console.error("Нет ответа от сервера:", error.request) + setError("Нет ответа от сервера") + } else { + console.error("Ошибка при настройке запроса:", error.message) + setError(`Ошибка: ${error.message}`) + } + + setLoading(false) + }) + }, [courseId, exerciseId]) useEffect(() => { let interval: NodeJS.Timeout | undefined @@ -37,6 +108,8 @@ export const Exercise = () => { setCurrentTime((prev) => { if (prev >= totalTime) { setIsPlaying(false) + // Отправляем результат на сервер при завершении + submitProgress() // Show completion animation history.push(getRouteCourseComplete()) return totalTime @@ -50,6 +123,23 @@ export const Exercise = () => { } }, [isPlaying, totalTime, history]) + // Функция для отправки прогресса на сервер + const submitProgress = async () => { + if (!courseId || !exerciseId) return + + try { + const timeUsers = formatTime(currentTime) // Отправляем время в формате MM:SS + + await connect.post(`pacient/${courseId}/${exerciseId}`, { + time_users: timeUsers, + }) + + console.log("Прогресс отправлен на сервер:", timeUsers) + } catch (error) { + console.error("Ошибка при отправке прогресса:", error) + } + } + const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60) const secs = seconds % 60 @@ -58,8 +148,6 @@ export const Exercise = () => { const progress = (currentTime / totalTime) * 100 - - const PlayIcon = () => ( @@ -73,7 +161,20 @@ export const Exercise = () => { ) const RefreshIcon = () => ( - + + + + ) const CheckIcon = () => ( @@ -82,52 +183,94 @@ export const Exercise = () => { ) + // Генерируем инструкции на основе реальных данных упражнения const exerciseSteps = [ { - title: "Исходное положение", - description: - "Лягте на спирку на коврик. Руки вдоль тела, ладони прижаты к полу. Ноги выпрямлены, носки направлены вверх. Поясница плотно прижата к полу.", + title: "Описание упражнения", + description: exercise?.desc || "Выполните упражнение согласно инструкции.", }, { - title: "Задание", - description: - "Медленно поднимите прямые ноги до угла 90 градусов. Задержитесь на 2 секунды, затем медленно опустите ноги, не касаясь пола. Повторите движение плавно и контролируемо.", + title: "Продолжительность", + description: `Время выполнения: ${exercise?.time || 15} минут`, }, { - title: "Подходы", - description: "Выполните 3 подхода по 12 повторений с отдыхом 60 секунд между подходами.", + title: "Подходы и повторения", + description: `Выполните ${exercise?.count || 1} подход по ${exercise?.repeats || 12} повторений с отдыхом 60 секунд между подходами.`, }, { - title: "Перерыв", - description: "Отдыхайте 60 секунд между подходами. Дышите спокойно и расслабьте мышцы.", + title: "Позиция в программе", + description: `Это упражнение №${exercise?.position || 1} в программе дня ${exercise?.day || 1}.`, }, { - title: "Динамика и статика", - description: - "Динамическая фаза: подъем и опускание ног выполняется плавно, 2 секунды вверх, 2 секунды вниз. Статическая фаза: удержание ног в верхней точке на 2 секунды.", + title: "Техника безопасности", + description: "Следите за правильной техникой выполнения. При появлении боли немедленно прекратите упражнение.", }, ] + if (loading) { + return ( +
+
+
+
+

Загрузка упражнения...

+
+
+
+ ) + } + + if (error) { + return ( +
+
+ +
+
+

{error}

+ +
+
+
+
+ ) + } + + if (!exercise) { + return ( +
+
+ +
+
+

Упражнение не найдено

+ +
+
+
+
+ ) + } return ( -
- - - +
-