оформление внутри курса

This commit is contained in:
Tatyana 2025-08-26 16:17:15 +03:00
commit c1a0e457d0
9 changed files with 803 additions and 141 deletions

View File

@ -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 { Exercise } from "./pages/Exercise";
import Settings from "./pages/Settings";
import CourseComplete from "./pages/CourseComplete";
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 { 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 { getRouteCourseExercises } from "./shared/consts/router";
// import { getRouteExercise } from "./shared/consts/router";
import { getRouteSettings } from "./shared/consts/router";
import { getRouteCourseComplete } from "./shared/consts/router";
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"
const AppRoutes = () => (
<Switch>
@ -30,18 +29,12 @@ const AppRoutes = () => (
<Route path={getRouteLogin()} component={Login} />
<Route path={getRouteHome()} component={Home} />
<Route path={getRouteForgotPassword()} component={ForgotPasword} />
<Route path={getRouteCourses()} component={Courses} />
{/* <Route path="pacient/:id" component={CourseExercises} /> */}
<Route path={getRouteCourseExercises(':id')} component={CourseExercises} />
<Route path="/pacient/:courseId/:exerciseId" component={Exercise} />
{/* <Route path={getRouteExercise(courseId, exerciseId)} component={Exercise} /> */}
<Route exact path={getRouteCourses()} component={Courses} />
<Route exact path="/course/:id" component={CourseExercises} />
<Route exact path="/course/:courseId/exercise/:exerciseId" component={Exercise} />
<Route path={getRouteSettings()} component={Settings} />
<Route path={getRouteCourseComplete()} component={CourseComplete} />
</Switch>
);
)
export default AppRoutes;
export default AppRoutes

View File

@ -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/',
baseURL: "http://localhost:8093/",
headers: {
'Content-type': 'application/json',
"Content-type": "application/json",
},
});
})
instance.interceptors.response.use(undefined, async (error) => {
if (error.response?.status === 401) {
localStorage.removeItem(AUTH_TOKEN);
localStorage.removeItem(USER_KEY);
localStorage.removeItem("authToken")
localStorage.removeItem("token")
localStorage.removeItem("userId")
localStorage.removeItem("userName")
// перехватывает запросы и брал пути из функции
window.location.href = getRouteLogin();
window.location.href = "/login"
}
return Promise.reject(error);
});
return Promise.reject(error)
})
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem(AUTH_TOKEN);
const c = config;
const token = localStorage.getItem("authToken") || localStorage.getItem("token")
const c = config
if (token && config) {
c.headers.Authorization = `Bearer ${token}`;
c.headers.Authorization = `Bearer ${token}`
}
return config;
return config
},
(error) => Promise.reject(error),
);
export { instance as connect };
)
export { instance as connect }

View File

@ -0,0 +1,288 @@
"use client"
import { useState, useEffect } from "react"
import { useHistory, useParams } from "react-router-dom"
import HeaderNav from "../components/HeaderNav"
import BottomNavigation from "../components/BottomNavigation"
import video from "../assets/img/video.mov"
import { connect } from '../confconnect';
import { getRouteExercise } from "../shared/consts/router"
interface CourseExercise {
ID: number;
title: string;
desc: string;
url_file_img: string;
}
export const CourseExercises = () => {
const history = useHistory()
// const { id } = useParams<{ id: string }>();
const [currentSlide, setCurrentSlide] = useState(0);
const token = localStorage.getItem('authToken');
useEffect(() => {
console.log(token)
if (!token) {
setError('Токен не найден');
return;
}
connect.get('/pacient/', {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(response => {
console.log('Response status:', response.status);
// Предполагаемая структура:
// response.data.courses — массив пользователей
const users = response.data.courses || [];
// Собираем все курсы из всех пользователей
const allCourses: Course[] = [];
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,
});
});
}
});
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}`);
}
});
}, [token]);
const course = {
id: 2,
description: 'Очень сильно',
name: 'Качаем ноги',
exercises: [
{
id: 1,
name: "Подъемы ног лежа",
duration: "15 мин",
sets: 3,
reps: 12,
image: "/placeholder.svg?height=200&width=300",
difficulty: "Легкий",
description: "Укрепление мышц бедра и улучшение подвижности коленного сустава",
calories: 45,
},
{
id: 2,
name: "Приседания у стены",
duration: "10 мин",
sets: 2,
reps: 15,
image: "/placeholder.svg?height=200&width=300",
difficulty: "Средний",
description: "Безопасные приседания для восстановления силы ног",
calories: 60,
},
{
id: 3,
name: "Растяжка квадрицепса",
duration: "8 мин",
sets: 1,
reps: 30,
image: "/placeholder.svg?height=200&width=300",
difficulty: "Легкий",
description: "Улучшение гибкости и снятие напряжения",
calories: 25,
},
{
id: 4,
name: "Укрепление икр",
duration: "12 мин",
sets: 3,
reps: 20,
image: "/placeholder.svg?height=200&width=300",
difficulty: "Средний",
description: "Развитие силы и выносливости икроножных мышц",
calories: 40,
},
]}
// Функции для переключения на следующее/предыдущее упражнение
const nextExercise = () => {
setCurrentSlide((prev) => (prev + 1) % course.exercises.length)
}
const prevExercise = () => {
setCurrentSlide((prev) => (prev - 1 + course.exercises.length) % course.exercises.length)
}
const currentExercise = course.exercises[currentSlide]
// 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-gray-400 to-gray-500"
// }
// }
return (
<div className="bg-gray-50 w-full h-full overflow-auto">
<div className="my-36 min-h-screen max-w-4xl mx-auto">
<HeaderNav item={course.name} text={course.description} />
{/* Exercise Slider */}
<div className="px-4 sm:px-6 mt-34 mb-10 ">
<div className="glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl relative">
{/* Exercise Image */}
<div className="relative">
<video
src={video}
className="w-full h-120 object-cover"
autoPlay
loop
muted
playsInline
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-black/10"></div>
{/* Difficulty Badge */}
<div className="absolute top-4 right-4">
{/* <div
className={`px-4 py-2 rounded-full text-xs font-bold text-white shadow-lg backdrop-blur-sm ${getDifficultyColor(currentExercise.difficulty)}`}
>
{currentExercise.difficulty}
</div> */}
</div>
{/* Navigation arrows */}
<button
onClick={prevExercise}
className="z-50 absolute left-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/20 backdrop-blur-xl rounded-full flex items-center justify-center text-white hover:bg-black/30 transition-all duration-300 shadow-lg border border-white/20"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
onClick={nextExercise}
className="z-50 absolute right-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/20 backdrop-blur-xl rounded-full flex items-center justify-center text-white hover:bg-black/30 transition-all duration-300 shadow-lg border border-white/20"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
{/* Exercise Info */}
<div className="p-6">
<h3 className="text-xl font-semibold text-gray-600 mb-3">{currentExercise.name}</h3>
<button
onClick={() => history.push(getRouteExercise(currentExercise.id))}
className="w-full bg-gradient-to-r bg-orange-400 text-white font-semibold py-4 px-6 rounded-2xl hover:shadow-2xl transition-all duration-300 transform hover:scale-105 shadow-lg backdrop-blur-sm"
>
Начать упражнение
</button>
</div>
</div>
{/* Slide indicators */}
<div className="flex justify-center mt-6 space-x-2">
{course.exercises.map((_, index) => (
<button
key={index}
onClick={() => setCurrentSlide(index)}
className={`h-4 rounded-full transition-all duration-300 ${index === currentSlide
? "bg-gradient-to-r from-[#2BACBE] to-cyan-600 w-8 shadow-lg"
: "bg-gray-300 w-4 hover:bg-gray-400"
}`}
/>
))}
</div>
</div>
{/* Exercise List */}
<div className="px-4 sm:px-6 mb-50">
<h2 className="text-xl sm:text-2xl font-semibold text-gray-800 mb-6">Все упражнения курса</h2>
<div className="space-y-4">
{course.exercises.map((exercise, index) => (
<div
key={exercise.id}
onClick={() => history.push(getRouteExercise(currentExercise.id))}
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" : ""
}`}
>
<div className="flex items-center space-x-4">
<div className="relative">
<div className="w-14 h-14 sm:w-16 sm:h-16 border-2 border-cyan-500 rounded-2xl flex items-center justify-center text-cyan-500 font-semibold text-lg sm:text-xl shadow-xl">
{index + 1}
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-800 text-lg sm:text-xl truncate">{exercise.name}</h3>
<p className="text-gray-600 text-sm mb-2 line-clamp-2">{exercise.description}</p>
</div>
<div className="text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
))}
</div>
</div>
<BottomNavigation />
</div>
</div>
)
}

View File

@ -98,6 +98,9 @@ export const Courses = () => {
});
}, [token]);
// Генерируем случайный прогресс для каждого курса
const getRandomProgress = () => Math.floor(Math.random() * 100);

240
src/pages/Exercise old.tsx Normal file
View File

@ -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 = () => (
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
)
const PauseIcon = () => (
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
)
const RefreshIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#6F6F6F" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> <path d="M2.5 2v6h6M21.5 22v-6h-6" /><path d="M22 11.5A10 10 0 0 0 3.2 7.2M2 12.5a10 10 0 0 0 18.8 4.2" /></svg>
)
const CheckIcon = () => (
<svg className="w-6 h-6 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)
const exerciseSteps = [
{
title: "Исходное положение",
description:
"Лягте на спирку на коврик. Руки вдоль тела, ладони прижаты к полу. Ноги выпрямлены, носки направлены вверх. Поясница плотно прижата к полу.",
},
{
title: "Задание",
description:
"Медленно поднимите прямые ноги до угла 90 градусов. Задержитесь на 2 секунды, затем медленно опустите ноги, не касаясь пола. Повторите движение плавно и контролируемо.",
},
{
title: "Подходы",
description: "Выполните 3 подхода по 12 повторений с отдыхом 60 секунд между подходами.",
},
{
title: "Перерыв",
description: "Отдыхайте 60 секунд между подходами. Дышите спокойно и расслабьте мышцы.",
},
{
title: "Динамика и статика",
description:
"Динамическая фаза: подъем и опускание ног выполняется плавно, 2 секунды вверх, 2 секунды вниз. Статическая фаза: удержание ног в верхней точке на 2 секунды.",
},
]
return (
<div className="bg-gray-50 w-full h-full overflow-auto">
<div className="mt-36 mb-90 min-h-screen max-w-4xl mx-auto">
<HeaderNav item='Надо разобраться?' text='упражнение' />
<div className="px-4 sm:px-6 mt-10 mb-6">
<div className="glass-morphism rounded-3xl overflow-hidden shadow-2xl border border-white/20 backdrop-blur-2xl">
<div className="relative">
<video
src={video}
className="w-full h-120 object-cover"
autoPlay
loop
muted
playsInline
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-black/10"></div>
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={() => setIsPlaying(!isPlaying)}
className={
isPlaying
? "w-20 h-20 bg-transparent backdrop-blur-xl rounded-full flex items-center justify-center shadow-2xl transition-all duration-300 transform hover:scale-110 border border-cyan-50 opacity-40"
: "rounded-full w-20 h-20 flex items-center justify-center backdrop-blur-xl opacity-70"
}
>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
</button>
</div>
{isPlaying && (
<div className="absolute top-4 left-4 flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-500 rounded-full animate-pulse "></div>
<span className="text-white text-sm font-bold bg-black/30 px-3 py-1 rounded-full backdrop-blur-sm">
Выполнение
</span>
</div>
)}
<div className="absolute top-4 right-4 bg-cyan-50 backdrop-blur-sm px-3 py-1 rounded-xl">
<span className="text-gray-800 text-sm font-bold">{formatTime(currentTime)}</span>
</div>
</div>
</div>
</div>
<div className="px-4 sm:px-6 space-y-4 mb-6">
{exerciseSteps.map((step, index) => (
<div
key={index}
className="bg-white rounded-2xl p-5 border border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300"
>
<div className="flex items-start space-x-4">
<div>
<h3 className="text-lg font-black text-gray-800 mb-2">{step.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm sm:text-base">{step.description}</p>
</div>
</div>
</div>
))}
</div>
{/* Fixed Timer at Bottom */}
<div className="fixed bottom-36 left-0 right-0 bg-white opacity-95 border-t border-white/20 px-4 sm:px-6 py-4 shadow-2xl z-30">
<div className="max-w-md mx-auto">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-sm font-bold text-gray-700">
Подход {currentSet} из {totalSets}
</span>
</div>
<span className="text-sm font-black text-gray-700">
{formatTime(currentTime)} / {formatTime(totalTime)}
</span>
</div>
<div className="bg-white/30 rounded-full h-3 mb-4 overflow-hidden">
<div
className="bg-gradient-to-r from-[#2BACBE] via-cyan-500 to-cyan-700 h-3 rounded-full transition-all duration-1000 shadow-sm"
style={{ width: `${progress}%` }}
></div>
</div>
<div className="flex space-x-3">
<button
onClick={() => setIsPlaying(!isPlaying)}
className={`flex-1 font-bold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 flex items-center justify-center space-x-2 ${isPlaying
? "bg-gray-400 text-white shadow-lg"
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white shadow-lg"
}`}
>
{isPlaying ? (
<>
<PauseIcon /> <span>Пауза</span>
</>
) : (
<>
<PlayIcon /> <span className="backdrop-blur-xl">Начать</span>
</>
)}
</button>
<button
onClick={() => {
setCurrentTime(0)
setIsPlaying(false)
}}
className="px-6 py-3 bg-white text-gray-800 font-bold rounded-xl transition-all duration-300 hover:shadow-lg border border-gray-200 flex items-center justify-center"
>
<RefreshIcon />
</button>
<button
onClick={() => setCurrentSet((prev) => Math.min(prev + 1, totalSets))}
className="px-6 py-3 bg-white text-gray-800 font-bold rounded-xl transition-all duration-300 hover:shadow-lg border border-gray-200 flex items-center justify-center"
>
<CheckIcon />
</button>
</div>
</div>
</div>
<BottomNavigation />
</div>
</div>
)
}

View File

@ -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<RouteParams>()
const [exercise, setExercise] = useState<Exercise | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string>("")
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 = () => (
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
@ -73,7 +161,20 @@ export const Exercise = () => {
)
const RefreshIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#6F6F6F" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> <path d="M2.5 2v6h6M21.5 22v-6h-6" /><path d="M22 11.5A10 10 0 0 0 3.2 7.2M2 12.5a10 10 0 0 0 18.8 4.2" /></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#6F6F6F"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2.5 2v6h6M21.5 22v-6h-6" />
<path d="M22 11.5A10 10 0 0 0 3.2 7.2M2 12.5a10 10 0 0 0 18.8 4.2" />
</svg>
)
const CheckIcon = () => (
@ -82,52 +183,94 @@ export const Exercise = () => {
</svg>
)
// Генерируем инструкции на основе реальных данных упражнения
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 (
<div className="bg-gray-50 w-full h-full overflow-auto">
<div className="mt-36 min-h-screen max-w-4xl mx-auto flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#2BACBE] mx-auto mb-4"></div>
<p className="text-gray-600">Загрузка упражнения...</p>
</div>
</div>
</div>
)
}
if (error) {
return (
<div className="bg-gray-50 w-full h-full overflow-auto">
<div className="mt-36 min-h-screen max-w-4xl mx-auto">
<HeaderNav item="Ошибка" text="Не удалось загрузить" />
<div className="px-6 mt-8">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<p className="text-red-600 font-medium mb-4">{error}</p>
<button
onClick={() => history.goBack()}
className="bg-[#2BACBE] text-white px-4 py-2 rounded-lg hover:bg-[#2A9FB8] transition-colors"
>
Вернуться назад
</button>
</div>
</div>
</div>
</div>
)
}
if (!exercise) {
return (
<div className="bg-gray-50 w-full h-full overflow-auto">
<div className="mt-36 min-h-screen max-w-4xl mx-auto">
<HeaderNav item="Упражнение" text="Не найдено" />
<div className="px-6 mt-8">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
<p className="text-gray-600 mb-4">Упражнение не найдено</p>
<button
onClick={() => history.goBack()}
className="bg-[#2BACBE] text-white px-4 py-2 rounded-lg hover:bg-[#2A9FB8] transition-colors"
>
Вернуться назад
</button>
</div>
</div>
</div>
</div>
)
}
return (
<div className="bg-gray-50 w-full h-full overflow-auto">
<div className="mt-36 mb-90 min-h-screen max-w-4xl mx-auto">
<HeaderNav item='Надо разобраться?' text='упражнение' />
<HeaderNav item={exercise.title} text={`День ${exercise.day}, позиция ${exercise.position}`} />
<div className="px-4 sm:px-6 mt-10 mb-6">
<div className="glass-morphism rounded-3xl overflow-hidden shadow-2xl border border-white/20 backdrop-blur-2xl">
<div className="relative">
<video
src={video}
className="w-full h-120 object-cover"
autoPlay
loop
muted
playsInline
/>
<img src={exercise.url_file_img || video} alt={exercise.title} className="w-full h-120 object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-black/10"></div>
<div className="absolute inset-0 flex items-center justify-center">
<button
@ -165,7 +308,6 @@ export const Exercise = () => {
className="bg-white rounded-2xl p-5 border border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300"
>
<div className="flex items-start space-x-4">
<div>
<h3 className="text-lg font-black text-gray-800 mb-2">{step.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm sm:text-base">{step.description}</p>
@ -198,7 +340,8 @@ export const Exercise = () => {
<div className="flex space-x-3">
<button
onClick={() => setIsPlaying(!isPlaying)}
className={`flex-1 font-bold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 flex items-center justify-center space-x-2 ${isPlaying
className={`flex-1 font-bold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 flex items-center justify-center space-x-2 ${
isPlaying
? "bg-gray-400 text-white shadow-lg"
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white shadow-lg"
}`}
@ -223,7 +366,10 @@ export const Exercise = () => {
<RefreshIcon />
</button>
<button
onClick={() => setCurrentSet((prev) => Math.min(prev + 1, totalSets))}
onClick={() => {
setCurrentSet((prev) => Math.min(prev + 1, totalSets))
submitProgress()
}}
className="px-6 py-3 bg-white text-gray-800 font-bold rounded-xl transition-all duration-300 hover:shadow-lg border border-gray-200 flex items-center justify-center"
>
<CheckIcon />
@ -236,5 +382,3 @@ export const Exercise = () => {
</div>
)
}

View File

@ -1,2 +1,2 @@
export const USER_KEY = 'user';
export const AUTH_TOKEN = 'token';
export const AUTH_TOKEN = "authToken"
export const USER_KEY = "userId"

View File

@ -1,22 +1,10 @@
export const getRouteWelcome = () => `/welcome`;
export const getRouteLogin = () => `/login`;
export const getRouteHome = () => `/home`;
export const getRouteForgotPassword = () => `/forgot-password`;
export const getRouteCourses = () => `/courses`;
export const getRouteCourseExercises = (id: number | string) => `/pacient/${id}`;
export const getRouteExercise = (courseId: number | string, execiseId: number | string) => `/pacient/${courseId}/${execiseId}`;
export const getRouteSettings = () => `/settings`;
export const getRouteCourseComplete = () => `/course-complete`;
export const getRouteWelcome = () => `/welcome`
export const getRouteLogin = () => `/login`
export const getRouteHome = () => `/home`
export const getRouteForgotPassword = () => `/forgot-password`
export const getRouteCourses = () => `/courses`
export const getRouteCourseExercises = (id: number | string) => `/course/${id}`
export const getRouteExercise = (courseId: number | string, exerciseId: number | string) =>
`/course/${courseId}/exercise/${exerciseId}`
export const getRouteSettings = () => `/settings`
export const getRouteCourseComplete = () => `/course-complete`

View File

@ -70,6 +70,17 @@ curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8093/pacient/courses
"sessionname": "krasnova@mail.ru"
curl -X GET \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxMTEyNDQsInVzZXJfZW1haWwiOiJrcmFzbm92YUBtYWlsLnJ1IiwidXNlcl9pZCI6MjUsInVzZXJfbmFtZSI6ImtyYXNub3ZhQG1haWwucnUiLCJ1c2VyX3JvbGUiOiIifQ.3UyamwJrLVkdjKgUG16lYyKm1jiZAv6MRQW0Tj2Z3Tc" \
http://localhost:8093/pacient/courses/:course_id
curl -L -X GET \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxMTUzMTAsInVzZXJfZW1haWwiOiJrcmFzbm92YUBtYWlsLnJ1IiwidXNlcl9pZCI6MjUsInVzZXJfbmFtZSI6ImtyYXNub3ZhQG1haWwucnUiLCJ1c2VyX3JvbGUiOiIifQ.fkP4uDp9TylgLcVZlb5Zs7Po48DdBk42E0Xf1sHsMwk" \
http://localhost:8093/pacient/courses/
ПОИСК ВНУТРИ КУРСА
curl -L -X GET \\
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxMTUzMTAsInVzZXJfZW1haWwiOiJrcmFzbm92YUBtYWlsLnJ1IiwidXNlcl9pZCI6MjUsInVzZXJfbmFtZSI6ImtyYXNub3ZhQG1haWwucnUiLCJ1c2VyX3JvbGUiOiIifQ.fkP4uDp9TylgLcVZlb5Zs7Po48DdBk42E0Xf1sHsMwk" \
http://localhost:8093/pacient/1
curl -L -X GET \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8093/pacient/course/1/