корректное отображение времени в секундах
This commit is contained in:
parent
a8b87fbade
commit
df8cc1596c
@ -6,22 +6,26 @@ interface CircularProgressDisplayProps {
|
||||
overallProgress: number
|
||||
totalCourses: number
|
||||
totalExercises: number
|
||||
completedCourses?: number
|
||||
completedExercises?: number
|
||||
}
|
||||
|
||||
const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
|
||||
overallProgress,
|
||||
totalCourses,
|
||||
totalExercises,
|
||||
completedCourses = 0,
|
||||
completedExercises = 0,
|
||||
}) => {
|
||||
const radius = 40
|
||||
const circumference = 2 * Math.PI * radius
|
||||
|
||||
// For the "Courses" ring (cyan)
|
||||
const coursesProgress = (totalCourses / 5) * 100 // Assuming max 5 courses for visual representation
|
||||
// For the "Courses" ring (orange) - shows completed courses progress
|
||||
const coursesProgress = totalCourses > 0 ? (completedCourses / totalCourses) * 100 : 0
|
||||
const coursesStrokeDashoffset = circumference - (coursesProgress / 100) * circumference
|
||||
|
||||
// For the "Exercises" ring (green)
|
||||
const exercisesProgress = (totalExercises / 50) * 100 // Assuming max 50 exercises for visual representation
|
||||
// For the "Exercises" ring (cyan) - shows completed exercises progress
|
||||
const exercisesProgress = totalExercises > 0 ? (completedExercises / totalExercises) * 100 : 0
|
||||
const exercisesStrokeDashoffset = circumference - (exercisesProgress / 100) * circumference
|
||||
|
||||
return (
|
||||
@ -38,7 +42,6 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
|
||||
cy="50"
|
||||
/>
|
||||
|
||||
|
||||
<circle
|
||||
className="text-orange-400"
|
||||
strokeWidth="8"
|
||||
@ -47,13 +50,13 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
|
||||
strokeLinecap="round"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
r={radius}
|
||||
r={radius - 10} // Slightly smaller radius for the inner ring
|
||||
|
||||
cx="50"
|
||||
cy="50"
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
|
||||
|
||||
<circle
|
||||
className="text-cyan-500"
|
||||
strokeWidth="8"
|
||||
@ -62,7 +65,7 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
|
||||
strokeLinecap="round"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
r={radius - 10} // Slightly smaller radius for the inner ring
|
||||
r={radius}
|
||||
cx="50"
|
||||
cy="50"
|
||||
transform="rotate(-90 50 50)"
|
||||
@ -70,7 +73,6 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
|
||||
</svg>
|
||||
<div className="absolute text-center">
|
||||
<div className="text-3xl font-black text-gray-600">{overallProgress}%</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -4,7 +4,6 @@
|
||||
// Если он взаимодействует с браузерными API или DOM.
|
||||
// Если он содержит обработчики событий или управляет состоянием.
|
||||
|
||||
|
||||
import type React from "react"
|
||||
|
||||
interface StatCardProps {
|
||||
@ -26,33 +25,18 @@ interface StatCardProps {
|
||||
// className?: string — необязственный CSS-класс.
|
||||
// style?: React.CSSProperties — необязательные inline-стили.
|
||||
|
||||
|
||||
|
||||
|
||||
export const StatCardHome: React.FC<StatCardProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
icon: Icon,
|
||||
fill,
|
||||
onClick,
|
||||
}) => {
|
||||
export const StatCardHome: React.FC<StatCardProps> = ({ title, subtitle, icon: Icon, fill, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="glass-morphism rounded-3xl text-left border border-white/50 shadow-lg backdrop-blur-xl p-6 text-white transition-transform transform hover:scale-105 duration-300 overflow-hidden cursor-pointer"
|
||||
>
|
||||
{/* Фоновая иконка */}
|
||||
<Icon
|
||||
size={180}
|
||||
fill={"#E6E5E5"}
|
||||
className="absolute -right-8 -top-8 -rotate-45"
|
||||
/>
|
||||
<Icon size={180} fill={"#E6E5E5"} className="absolute -right-8 -top-8 -rotate-45" />
|
||||
|
||||
{/* Основная иконка */}
|
||||
<div className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center shadow-xl mb-4 z-20 relative">
|
||||
<Icon size={40}
|
||||
fill={fill}
|
||||
/>
|
||||
<Icon size={40} fill={fill} />
|
||||
</div>
|
||||
|
||||
{/* Текст */}
|
||||
|
@ -2,14 +2,39 @@
|
||||
|
||||
import type React from "react"
|
||||
|
||||
interface CurrentExercise {
|
||||
courseId: string
|
||||
exerciseId: string
|
||||
exerciseIndex: number
|
||||
day: number
|
||||
title: string
|
||||
position: number
|
||||
isCompleted: boolean
|
||||
}
|
||||
|
||||
interface WorkoutCardProps {
|
||||
onBackClick: () => void
|
||||
onCardClick: () => void
|
||||
currentExercise?: CurrentExercise | null
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// Это означает, что компонент ожидает получить пропс onBackClick, который должен быть функцией, вызываемой при определенном событии (например, при нажатии кнопки "Назад").
|
||||
export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCardClick, currentExercise, loading }) => {
|
||||
const getExerciseStatus = () => {
|
||||
if (loading) return "Загрузка..."
|
||||
if (!currentExercise) return "Нет активных упражнений"
|
||||
if (currentExercise.isCompleted) return "Все упражнения завершены"
|
||||
return "В процессе"
|
||||
}
|
||||
|
||||
const getExerciseDescription = () => {
|
||||
if (loading) return "Поиск текущего упражнения..."
|
||||
if (!currentExercise) return "Начните новый курс"
|
||||
if (currentExercise.isCompleted) return "Отличная работа!"
|
||||
return `Курс ${currentExercise.courseId} / День ${currentExercise.day}`
|
||||
|
||||
}
|
||||
|
||||
export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCardClick }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onCardClick}
|
||||
@ -51,7 +76,11 @@ export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCar
|
||||
</svg>
|
||||
</div>
|
||||
{/* Пульсирующая точка */}
|
||||
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-white rounded-full shadow-lg animate-pulse flex items-center justify-center">
|
||||
<div
|
||||
className={`absolute -bottom-1 -right-1 w-6 h-6 bg-white rounded-full shadow-lg flex items-center justify-center ${
|
||||
loading ? "animate-pulse" : currentExercise && !currentExercise.isCompleted ? "animate-pulse" : ""
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3 h-3 text-[#2BACBE]" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
@ -60,8 +89,8 @@ export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCar
|
||||
|
||||
{/* Информация о упражнении */}
|
||||
<div className="flex flex-col justify-center flex-grow">
|
||||
<h3 className="font-extrabold text-lg mb-2">В процессе</h3>
|
||||
<p className="text-white/70 font-medium mb-4 text-base">Текущее упражнение</p>
|
||||
<h3 className="font-extrabold text-lg mb-2">{getExerciseStatus()}</h3>
|
||||
<p className="text-white/70 font-medium mb-4 text-base">{getExerciseDescription()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../src/assets/fonts/Poppins-Semibold.woff2') format('woff2');
|
||||
src: url('../src/assets/fonts/Poppins-SemiBold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -1,100 +1,139 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useState, useEffect } from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
|
||||
import BottomNavigation from "../components/BottomNavigation";
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
|
||||
import HeaderNav from "../components/HeaderNav";
|
||||
import { connect } from '../confconnect';
|
||||
import { getRouteCourseExercises } from '../shared/consts/router';
|
||||
import HeaderNav from "../components/HeaderNav"
|
||||
import { connect } from "../confconnect"
|
||||
import { getRouteCourseExercises } from "../shared/consts/router"
|
||||
|
||||
import type { CourseExercises } from './CourseExercises';
|
||||
import type { CourseExercises } from "./CourseExercises"
|
||||
|
||||
export interface Course {
|
||||
ID: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
url_file_img: string;
|
||||
course_exercises: CourseExercises;
|
||||
ID: number
|
||||
title: string
|
||||
desc: string
|
||||
url_file_img: string
|
||||
course_exercises: CourseExercises
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
courses: User[];
|
||||
course_exercises: CourseExercises;
|
||||
courses: User[]
|
||||
course_exercises: CourseExercises
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
Courses?: Course[];
|
||||
id: number
|
||||
name: string
|
||||
Courses?: Course[]
|
||||
}
|
||||
|
||||
|
||||
const ProgressLine = () => {
|
||||
const ProgressLine = ({ progress }: { progress: number }) => {
|
||||
return (
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-300 to-orange-500 transition-all duration-500 absolute left-0 top-0 rounded-l-2xl"
|
||||
style={{ width: '85%' }}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const AnalitcsCards = () => {
|
||||
const AnalitcsCards = ({ overallProgress }: { overallProgress: number }) => {
|
||||
return (
|
||||
<div className="px-6 mb-8">
|
||||
<div className="text-center relative ">
|
||||
<div className="w-full h-10 bg-gray-500 rounded-2xl relative flex items-center justify-center mb-4 shadow-xl">
|
||||
{/* Прогрессная часть */}
|
||||
<ProgressLine />
|
||||
<ProgressLine progress={overallProgress} />
|
||||
|
||||
{/* Текст поверх линии */}
|
||||
<div className="absolute text-white font-semibold text-sm sm:text-base">Вы прошли реабилитацию на {85}%</div>
|
||||
<div className="absolute text-white font-semibold text-sm sm:text-base">
|
||||
Вы прошли реабилитацию на {overallProgress}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const calculateCourseProgress = async (course: Course, token: string): Promise<number> => {
|
||||
try {
|
||||
const response = await connect.get(`/pacient/${course.ID}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
const exercises = response.data.course_exercises || []
|
||||
if (exercises.length === 0) return 0
|
||||
|
||||
let completedExercises = 0
|
||||
|
||||
for (const exercise of exercises) {
|
||||
const storageKey = `exerciseProgress_${course.ID}_${exercise.id_exercise}_day_${exercise.day}`
|
||||
const savedProgress = localStorage.getItem(storageKey)
|
||||
|
||||
if (savedProgress) {
|
||||
const progress = JSON.parse(savedProgress)
|
||||
const isCompleted =
|
||||
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
|
||||
|
||||
if (isCompleted) {
|
||||
completedExercises++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Math.round((completedExercises / exercises.length) * 100)
|
||||
} catch (error) {
|
||||
console.error(`Ошибка при расчете прогресса курса ${course.ID}:`, error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// НАЧАЛО КОМПОНЕНТА
|
||||
|
||||
export const Courses = () => {
|
||||
const history = useHistory();
|
||||
const history = useHistory()
|
||||
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [error, setError] = useState<string>('');
|
||||
const token = localStorage.getItem('authToken');
|
||||
const [courses, setCourses] = useState<Course[]>([])
|
||||
const [error, setError] = useState<string>("")
|
||||
const token = localStorage.getItem("authToken")
|
||||
|
||||
const [, setExercises] = useState<CourseExercises[]>([]);
|
||||
const [, setExercises] = useState<CourseExercises[]>([])
|
||||
|
||||
const [courseProgress, setCourseProgress] = useState<{ [key: number]: number }>({})
|
||||
const [overallProgress, setOverallProgress] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
console.log(token)
|
||||
if (!token) {
|
||||
setError('Токен не найден');
|
||||
return;
|
||||
setError("Токен не найден")
|
||||
return
|
||||
}
|
||||
|
||||
connect.get('/pacient/courses', {
|
||||
connect
|
||||
.get("/pacient/courses", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then((response: { data: ResponseData }) => {
|
||||
console.log('Response status:', response.data);
|
||||
setExercises(response.data.courses.course_exercises);
|
||||
console.log("Response status:", response.data)
|
||||
setExercises(response.data.courses.course_exercises)
|
||||
|
||||
// Предполагаемая структура:
|
||||
// response.data.courses — массив пользователей
|
||||
const users = response.data.courses || [];
|
||||
const users = response.data.courses || []
|
||||
|
||||
// Собираем все курсы из всех пользователей
|
||||
const allCourses: Course[] = [];
|
||||
const allCourses: Course[] = []
|
||||
|
||||
users.forEach(user => {
|
||||
users.forEach((user) => {
|
||||
if (user.Courses && Array.isArray(user.Courses)) {
|
||||
user.Courses.forEach(course => {
|
||||
user.Courses.forEach((course) => {
|
||||
// Можно добавить проверку или преобразование
|
||||
allCourses.push({
|
||||
ID: course.id, // или course.ID, зависит от структуры
|
||||
@ -102,46 +141,56 @@ export const Courses = () => {
|
||||
desc: course.desc,
|
||||
url_file_img: course.url_file_img,
|
||||
course_exercises: course.course_exercises,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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]);
|
||||
})
|
||||
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (courses.length > 0 && token) {
|
||||
const calculateAllProgress = async () => {
|
||||
const progressMap: { [key: number]: number } = {}
|
||||
let totalProgress = 0
|
||||
|
||||
// Генерируем случайный прогресс для каждого курса
|
||||
const getRandomProgress = () => Math.floor(Math.random() * 100);
|
||||
for (const course of courses) {
|
||||
const progress = await calculateCourseProgress(course, token)
|
||||
progressMap[course.ID] = progress
|
||||
totalProgress += progress
|
||||
}
|
||||
|
||||
setCourseProgress(progressMap)
|
||||
setOverallProgress(Math.round(totalProgress / courses.length))
|
||||
}
|
||||
|
||||
calculateAllProgress()
|
||||
}
|
||||
}, [courses, token])
|
||||
|
||||
// Цвета для прогресс-баров в оттенках cyan
|
||||
const progressColors = [
|
||||
"from-cyan-600 to-cyan-900",
|
||||
];
|
||||
const progressColors = ["from-cyan-600 to-cyan-900"]
|
||||
|
||||
//item.exercise.title
|
||||
|
||||
|
||||
return (
|
||||
<div className="my-36 min-h-screen max-w-4xl mx-auto">
|
||||
<HeaderNav item='Курсы' text='все назначенные' />
|
||||
<AnalitcsCards />
|
||||
|
||||
<HeaderNav item="Курсы" text="все назначенные" />
|
||||
<AnalitcsCards overallProgress={overallProgress} />
|
||||
|
||||
<div className="px-6 mb-8">
|
||||
{error && (
|
||||
@ -157,11 +206,11 @@ export const Courses = () => {
|
||||
</div>
|
||||
|
||||
{/* Выводим список курсов */}
|
||||
<div className='space-y-4'>
|
||||
{courses.length > 0 ? (
|
||||
courses.map((course, index) => {
|
||||
const progress = getRandomProgress();
|
||||
const colorClass = progressColors[index % progressColors.length];
|
||||
<div className="space-y-4">
|
||||
{courses.length > 0
|
||||
? courses.map((course, index) => {
|
||||
const progress = courseProgress[course.ID] || 0
|
||||
const colorClass = progressColors[index % progressColors.length]
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -188,9 +237,7 @@ export const Courses = () => {
|
||||
<h3 className="font-semibold text-[#5F5C5C] text-lg mb-2">{course.title}</h3>
|
||||
|
||||
{/* Описание курса */}
|
||||
{course.desc && (
|
||||
<p className="text-sm text-[#5F5C5C]/60 mb-3 line-clamp-2">{course.desc}</p>
|
||||
)}
|
||||
{course.desc && <p className="text-sm text-[#5F5C5C]/60 mb-3 line-clamp-2">{course.desc}</p>}
|
||||
|
||||
{/* Индикатор прогресса */}
|
||||
<div className="bg-white/50 rounded-full h-3 mb-2 overflow-hidden">
|
||||
@ -217,8 +264,7 @@ export const Courses = () => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
!error && (
|
||||
: !error && (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
|
||||
<p className="text-gray-600 mb-4">У вас пока нет назначенных курсов</p>
|
||||
<button
|
||||
@ -228,12 +274,11 @@ export const Courses = () => {
|
||||
Просмотреть доступные курсы
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ export const Exercise = () => {
|
||||
const { courseId, exerciseId, exerciseIndex } = useParams<RouteParams>()
|
||||
const location = useLocation()
|
||||
|
||||
|
||||
const [course_exercises, setCourseExercises] = useState<CourseExercises[]>([])
|
||||
const [actualExerciseId, setActualExerciseId] = useState<string>("")
|
||||
const [currentDay, setCurrentDay] = useState<number | null>(null)
|
||||
@ -68,7 +67,7 @@ export const Exercise = () => {
|
||||
// ========== СОСТОЯНИЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ==========
|
||||
const [isResting, setIsResting] = useState(false)
|
||||
const [restTime, setRestTime] = useState(0)
|
||||
const [totalRestTime] = useState(60)
|
||||
const [totalRestTime] = useState(10) // Set default rest time to 60 seconds
|
||||
const [isRestPaused, setIsRestPaused] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@ -154,7 +153,14 @@ export const Exercise = () => {
|
||||
})
|
||||
|
||||
if (exerciseData.time) {
|
||||
const timeInSeconds = Number.parseInt(exerciseData.time) * 60
|
||||
let timeInSeconds: number
|
||||
if (typeof exerciseData.time === "string" && exerciseData.time.includes(":")) {
|
||||
// Time is in MM:SS format, convert to seconds
|
||||
timeInSeconds = convertTimeToSeconds(exerciseData.time)
|
||||
} else {
|
||||
// Time is already in seconds
|
||||
timeInSeconds = Number.parseInt(exerciseData.time)
|
||||
}
|
||||
setTotalTime(timeInSeconds)
|
||||
console.log("Установлено время упражнения:", timeInSeconds, "секунд")
|
||||
}
|
||||
@ -198,7 +204,6 @@ export const Exercise = () => {
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
console.log("Ответ сервера с прогрессом:", response.data)
|
||||
|
||||
if (
|
||||
@ -213,7 +218,7 @@ export const Exercise = () => {
|
||||
const matchesCourse = record.course_id === Number.parseInt(courseId)
|
||||
const matchesDay = currentDay ? record.day === currentDay : true
|
||||
console.log(
|
||||
"[v0] Filtering progress - Course match:",
|
||||
"Filtering progress - Course match:",
|
||||
matchesCourse,
|
||||
"Day match:",
|
||||
matchesDay,
|
||||
@ -249,7 +254,12 @@ export const Exercise = () => {
|
||||
}
|
||||
|
||||
if (lastIncompleteSet) {
|
||||
const timeInSeconds = convertTimeToSeconds(lastSavedTime)
|
||||
let timeInSeconds: number
|
||||
if (typeof lastSavedTime === "string" && lastSavedTime.includes(":")) {
|
||||
timeInSeconds = convertTimeToSeconds(lastSavedTime)
|
||||
} else {
|
||||
timeInSeconds = Number.parseInt(lastSavedTime) || 0
|
||||
}
|
||||
|
||||
setCurrentSet(lastIncompleteSet.set)
|
||||
setCurrentTime(timeInSeconds)
|
||||
@ -303,9 +313,9 @@ export const Exercise = () => {
|
||||
const saveSetProgress = async (setNumber: number, timeString: string, status: number) => {
|
||||
try {
|
||||
const dayToSave = currentDay || 1
|
||||
console.log(`[v0] SAVING PROGRESS: Set ${setNumber}, Status ${status}, Day ${dayToSave}`)
|
||||
console.log(`[v0] Current state - currentDay: ${currentDay}, dayToSave: ${dayToSave}`)
|
||||
console.log(`[v0] URL search params:`, location.search)
|
||||
console.log(`[SAVING PROGRESS: Set ${setNumber}, Status ${status}, Day ${dayToSave}`)
|
||||
console.log(`[Current state - currentDay: ${currentDay}, dayToSave: ${dayToSave}`)
|
||||
console.log(`[URL search params:`, location.search)
|
||||
|
||||
const progressData = {
|
||||
time_users: timeString,
|
||||
@ -327,21 +337,13 @@ export const Exercise = () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
console.log("TEST DAY COMPLETE", course_exercises)
|
||||
console.log("Всего упражнений в курсе", course_exercises.length)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ========== ФУНКЦИЯ перехода к след упражнению ==========
|
||||
const goToNextExercise = () => {
|
||||
console.log("Переходим к следующему упражнению")
|
||||
|
||||
|
||||
if (exerciseIndex !== undefined) {
|
||||
const currentIndex = Number.parseInt(exerciseIndex)
|
||||
const nextIndex = currentIndex + 1
|
||||
@ -349,8 +351,6 @@ export const Exercise = () => {
|
||||
const dayParam = selectedDay ? `?day=${selectedDay}` : ""
|
||||
|
||||
history.push(`/course/${courseId}/exercise/${nextIndex}${dayParam}`)
|
||||
|
||||
|
||||
} else {
|
||||
const currentExerciseNum = Number.parseInt(actualExerciseId)
|
||||
const nextExerciseId = currentExerciseNum + 1
|
||||
@ -584,8 +584,9 @@ export const Exercise = () => {
|
||||
|
||||
// ========== ФУНКЦИЯ ФОРМАТИРОВАНИЯ ВРЕМЕНИ ==========
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
const totalSeconds = Math.round(seconds)
|
||||
const mins = Math.floor(totalSeconds / 60)
|
||||
const secs = totalSeconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
@ -873,7 +874,6 @@ export const Exercise = () => {
|
||||
<span>Следующее упражнение</span>
|
||||
</button>
|
||||
|
||||
|
||||
<div className="px-4 py-3 bg-cyan-500 text-white font-bold rounded-xl flex items-center justify-center space-x-2">
|
||||
<CheckIcon />
|
||||
<span>Завершено</span>
|
||||
|
@ -1,49 +1,242 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useEffect, useState } from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
// import { useParams } from "react-router-dom"
|
||||
|
||||
import { CalendarIcon } from "../components/icons/CalendarIcon";
|
||||
import { DumbbellIcon } from "../components/icons/DumbbellIcon";
|
||||
import { CalendarIcon } from "../components/icons/CalendarIcon"
|
||||
import { DumbbellIcon } from "../components/icons/DumbbellIcon"
|
||||
|
||||
import HeaderNav from "../components/HeaderNav"
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
import CircularProgressDisplay from "../components/CircularProgressDisplay"
|
||||
|
||||
import HeaderNav from "../components/HeaderNav";
|
||||
import BottomNavigation from "../components/BottomNavigation";
|
||||
import CircularProgressDisplay from "../components/CircularProgressDisplay";
|
||||
import { StatCardHome } from "../components/cards/StatCardHome"
|
||||
import { WorkoutCardHome } from "../components/cards/WorkoutCardHome"
|
||||
|
||||
import { connect } from "../confconnect"
|
||||
import { getRouteExerciseByIndex } from "../shared/consts/router"
|
||||
import { getRouteCourses } from "../shared/consts/router"
|
||||
import { getRouteCourseExercises } from "../shared/consts/router"
|
||||
|
||||
import { StatCardHome } from "../components/cards/StatCardHome";
|
||||
import { WorkoutCardHome } from "../components/cards/WorkoutCardHome";
|
||||
import type { Course, User, CoursesApiResponse } from "../types/course"
|
||||
|
||||
import { connect } from '../confconnect';
|
||||
import { getRouteExerciseByIndex } from "../shared/consts/router";
|
||||
import { getRouteCourses } from "../shared/consts/router";
|
||||
import { getRouteCourseExercises } from "../shared/consts/router";
|
||||
interface CourseExercises {
|
||||
id_exercise: number
|
||||
day: number
|
||||
position: number
|
||||
title: string
|
||||
desc: string
|
||||
time: string
|
||||
count: number
|
||||
repeats: number
|
||||
}
|
||||
|
||||
import type { Course, User, CoursesApiResponse } from "../types/course";
|
||||
interface CurrentExercise {
|
||||
courseId: string
|
||||
exerciseId: string
|
||||
exerciseIndex: number
|
||||
day: number
|
||||
title: string
|
||||
position: number
|
||||
isCompleted: boolean
|
||||
}
|
||||
|
||||
interface ProgressStats {
|
||||
completedExercises: number
|
||||
totalExercises: number
|
||||
completedCourses: number
|
||||
totalCourses: number
|
||||
overallProgress: number
|
||||
}
|
||||
|
||||
// interface RouteParams {
|
||||
// courseId: string
|
||||
// exerciseId?: string // Made optional since we might have exerciseIndex instead
|
||||
// exerciseIndex?: string // Added exerciseIndex parameter
|
||||
// }
|
||||
|
||||
//НАЧАЛО //
|
||||
export default function Home() {
|
||||
const history = useHistory()
|
||||
const [currentDate, setCurrentDate] = useState("")
|
||||
const [error, setError] = useState<string>("")
|
||||
const [courses, setCourses] = useState<Course[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const history = useHistory();
|
||||
const [currentDate, setCurrentDate] = useState("");
|
||||
const [, setError] = useState<string>('');
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentExercise, setCurrentExercise] = useState<CurrentExercise | null>(null)
|
||||
const [loadingCurrentExercise, setLoadingCurrentExercise] = useState(false)
|
||||
// const { courseId, exerciseId, exerciseIndex } = useParams<RouteParams>()
|
||||
|
||||
const token = localStorage.getItem('authToken');
|
||||
const [progressStats, setProgressStats] = useState<ProgressStats>({
|
||||
completedExercises: 0,
|
||||
totalExercises: 0,
|
||||
completedCourses: 0,
|
||||
totalCourses: 0,
|
||||
overallProgress: 0,
|
||||
})
|
||||
|
||||
const exerciseProgress = localStorage.getItem('exerciseProgress');
|
||||
const currentProgress = localStorage.getItem('currentProgress')
|
||||
const token = localStorage.getItem("authToken")
|
||||
|
||||
const calculateProgressStats = async (coursesList: Course[]): Promise<ProgressStats> => {
|
||||
let totalExercises = 0
|
||||
let completedExercises = 0
|
||||
let completedCourses = 0
|
||||
|
||||
for (const course of coursesList) {
|
||||
try {
|
||||
// Получаем упражнения для каждого курса
|
||||
const response = await connect.get(`/pacient/${course.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
const exercises = response.data.course_exercises || []
|
||||
totalExercises += exercises.length
|
||||
|
||||
console.log("упражнения для каждого курса", response.data)
|
||||
|
||||
let courseCompletedExercises = 0
|
||||
|
||||
// Проверяем каждое упражнение на завершенность
|
||||
for (const exercise of exercises) {
|
||||
const storageKey = `exerciseProgress_${course.id}_${exercise.id_exercise}_day_${exercise.day}`
|
||||
const savedProgress = localStorage.getItem(storageKey)
|
||||
|
||||
if (savedProgress) {
|
||||
const progress = JSON.parse(savedProgress)
|
||||
const isCompleted =
|
||||
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
|
||||
|
||||
if (isCompleted) {
|
||||
completedExercises++
|
||||
courseCompletedExercises++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Курс считается завершенным если все упражнения выполнены
|
||||
if (exercises.length > 0 && courseCompletedExercises === exercises.length) {
|
||||
completedCourses++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Ошибка при получении упражнений для курса ${course.id}:`, error)
|
||||
// Если не удалось загрузить упражнения, считаем что в курсе 0 упражнений
|
||||
}
|
||||
}
|
||||
|
||||
const overallProgress = totalExercises > 0 ? Math.round((completedExercises / totalExercises) * 100) : 0
|
||||
|
||||
return {
|
||||
completedExercises,
|
||||
totalExercises,
|
||||
completedCourses,
|
||||
totalCourses: coursesList.length,
|
||||
overallProgress,
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentExercise = async () => {
|
||||
if (courses.length === 0) return
|
||||
|
||||
setLoadingCurrentExercise(true)
|
||||
|
||||
try {
|
||||
// Берем первый доступный курс
|
||||
const firstCourse = courses[0]
|
||||
|
||||
// Загружаем упражнения курса
|
||||
const response = await connect.get(`/pacient/${firstCourse.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
const exercises = response.data.course_exercises || []
|
||||
|
||||
if (exercises.length === 0) {
|
||||
setCurrentExercise(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Группируем упражнения по дням
|
||||
const exercisesByDay = exercises.reduce((acc: any, exercise: CourseExercises) => {
|
||||
if (!acc[exercise.day]) {
|
||||
acc[exercise.day] = []
|
||||
}
|
||||
acc[exercise.day].push(exercise)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Ищем первое незавершенное упражнение
|
||||
let foundExercise: CurrentExercise | null = null
|
||||
|
||||
for (const day of Object.keys(exercisesByDay).sort((a, b) => Number(a) - Number(b))) {
|
||||
const dayExercises = exercisesByDay[day].sort(
|
||||
(a: CourseExercises, b: CourseExercises) => a.position - b.position,
|
||||
)
|
||||
|
||||
for (let i = 0; i < dayExercises.length; i++) {
|
||||
const exercise = dayExercises[i]
|
||||
|
||||
// Проверяем прогресс упражнения в localStorage
|
||||
const storageKey = `exerciseProgress_${firstCourse.id}_${exercise.id_exercise}_day_${day}`
|
||||
const savedProgress = localStorage.getItem(storageKey)
|
||||
|
||||
let isCompleted = false
|
||||
if (savedProgress) {
|
||||
const progress = JSON.parse(savedProgress)
|
||||
isCompleted =
|
||||
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
|
||||
}
|
||||
|
||||
if (!isCompleted) {
|
||||
foundExercise = {
|
||||
courseId: firstCourse.id.toString(),
|
||||
exerciseId: exercise.id_exercise.toString(),
|
||||
exerciseIndex: i,
|
||||
day: Number(day),
|
||||
title: exercise.title,
|
||||
position: exercise.position,
|
||||
isCompleted: false,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundExercise) break
|
||||
}
|
||||
|
||||
// Если все упражнения завершены, берем последнее
|
||||
if (!foundExercise && exercises.length > 0) {
|
||||
const lastExercise = exercises[exercises.length - 1]
|
||||
foundExercise = {
|
||||
courseId: firstCourse.id.toString(),
|
||||
exerciseId: lastExercise.id_exercise.toString(),
|
||||
exerciseIndex: exercises.length - 1,
|
||||
day: lastExercise.day,
|
||||
title: lastExercise.title,
|
||||
position: lastExercise.position,
|
||||
isCompleted: true,
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentExercise(foundExercise)
|
||||
} catch (error) {
|
||||
console.error("Ошибка при получении текущего упражнения:", error)
|
||||
} finally {
|
||||
setLoadingCurrentExercise(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
console.log(token)
|
||||
if (!token) {
|
||||
setError('Токен не найден');
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("Токен не найден")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentDate(
|
||||
@ -67,7 +260,7 @@ export default function Home() {
|
||||
if (user.Courses && Array.isArray(user.Courses)) {
|
||||
user.Courses.forEach((course) => {
|
||||
allCourses.push({
|
||||
ID: course.ID,
|
||||
id: course.id,
|
||||
title: course.title,
|
||||
desc: course.desc,
|
||||
url_file_img: course.url_file_img,
|
||||
@ -97,21 +290,26 @@ export default function Home() {
|
||||
})
|
||||
}, [token])
|
||||
|
||||
// Расчет статистики / выполненные где брать???
|
||||
const totalCourses = courses.length
|
||||
const totalExercises = courses.reduce((sum, course) => {
|
||||
if (course.course_exercises && Array.isArray(course.course_exercises)) {
|
||||
return sum + course.course_exercises.length
|
||||
useEffect(() => {
|
||||
if (courses.length > 0 && !loading) {
|
||||
getCurrentExercise()
|
||||
|
||||
calculateProgressStats(courses).then((stats) => {
|
||||
setProgressStats(stats)
|
||||
})
|
||||
}
|
||||
return sum + Math.floor(Math.random() * 10) + 5
|
||||
}, 0)
|
||||
|
||||
const overallProgress = courses.length > 0 ? Math.floor(Math.random() * 100) : 0
|
||||
|
||||
//пока передаю тестовые значения
|
||||
}, [courses, loading])
|
||||
|
||||
const handleWorkoutClick = () => {
|
||||
history.push(getRouteExerciseByIndex(1,1,1))
|
||||
if (currentExercise) {
|
||||
// Используем правильный роут с day параметром
|
||||
history.push(
|
||||
getRouteExerciseByIndex(currentExercise.courseId, currentExercise.exerciseIndex, currentExercise.day),
|
||||
)
|
||||
} else {
|
||||
// Fallback если нет текущего упражнения
|
||||
history.push(getRouteExerciseByIndex(1, 0, 1))
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackClick = () => {
|
||||
@ -124,7 +322,7 @@ export default function Home() {
|
||||
|
||||
const handleExercisesClick = () => {
|
||||
if (courses.length > 0) {
|
||||
history.push(getRouteCourseExercises(`${'1'}`))
|
||||
history.push(getRouteCourseExercises(courses[0].id))
|
||||
} else {
|
||||
history.push(getRouteCourses())
|
||||
}
|
||||
@ -144,48 +342,49 @@ export default function Home() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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="Прогресс" text={currentDate} />
|
||||
|
||||
<div className="bg-white rounded-3xl p-6 shadow-lg mx-4 sm:mx-6">
|
||||
<div className="flex content-center items-center justify-between ">
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div className="flex content-center items-center justify-between"></div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="text-left">
|
||||
<div className="text-sm sm:text-base text-gray-800">Все курсы</div>
|
||||
<div className="text-2xl font-bold text-cyan-500">{totalCourses}/?</div>
|
||||
<div className="text-2xl font-bold text-orange-500">
|
||||
{progressStats.completedCourses}/{progressStats.totalCourses}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-sm sm:text-base text-gray-800">Все упражнения</div>
|
||||
<div className="text-2xl font-bold text-orange-400">{totalExercises}/?</div>
|
||||
<div className="text-2xl font-bold text-cyan-400">
|
||||
{progressStats.completedExercises}/{progressStats.totalExercises}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-8">
|
||||
<CircularProgressDisplay
|
||||
overallProgress={overallProgress}
|
||||
totalCourses={totalCourses}
|
||||
totalExercises={totalExercises}
|
||||
overallProgress={progressStats.overallProgress}
|
||||
totalCourses={progressStats.totalCourses}
|
||||
totalExercises={progressStats.totalExercises}
|
||||
completedCourses={progressStats.completedCourses}
|
||||
completedExercises={progressStats.completedExercises}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 space-y-6">
|
||||
{/* Текущее упражнение */}
|
||||
<WorkoutCardHome onBackClick={handleBackClick} onCardClick={handleWorkoutClick}
|
||||
<WorkoutCardHome
|
||||
onBackClick={handleBackClick}
|
||||
onCardClick={handleWorkoutClick}
|
||||
currentExercise={currentExercise}
|
||||
loading={loadingCurrentExercise}
|
||||
/>
|
||||
|
||||
|
||||
{/* Quick Stats (Total Exercises & Total Courses) */}
|
||||
<div className="grid grid-cols-2 gap-4 md:gap-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-5">
|
||||
<StatCardHome
|
||||
title="Курсы"
|
||||
subtitle="назначенные"
|
||||
@ -201,27 +400,6 @@ export default function Home() {
|
||||
onClick={handleExercisesClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-100">
|
||||
<p className="font-bold ">Взяли из локального хранилища:</p>
|
||||
надо считать прогресс исходя из выполненных/ вопрос: то есть, если выполнено, то как его исключить / как флаг проставлять?==
|
||||
все курсы мы вяли из ручки курсов, назначенные по роутингу (пути в router.ts)
|
||||
|
||||
надо достать текущий курс. который не выполнен.
|
||||
|
||||
как узнать, что курс выполнен?==
|
||||
|
||||
мы берем данные для текущей тренировки, которые записываются с помощью post, --оттуда достаем данные для текущего упражнения, которое записано (1 строка в БД всегда),
|
||||
а потом переходим на это упражнение.
|
||||
|
||||
и еще - мы считаем по индексу отображение, это же не повлияет на вывод итоговый на домашней странице, например?
|
||||
|
||||
|
||||
<div className="bg-red-200">
|
||||
<p>exerciseProgress:{exerciseProgress}</p>
|
||||
<p>currentProgress{currentProgress}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
|
@ -8,7 +8,7 @@ export interface CourseExercise {
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
ID: number
|
||||
id: number
|
||||
title: string
|
||||
desc: string
|
||||
url_file_img: string
|
||||
|
Loading…
x
Reference in New Issue
Block a user