корректное отображение времени в секундах

This commit is contained in:
Tatyana 2025-09-08 14:46:31 +03:00
parent a8b87fbade
commit df8cc1596c
8 changed files with 551 additions and 313 deletions

View File

@ -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>
)

View File

@ -4,14 +4,13 @@
// Если он взаимодействует с браузерными API или DOM.
// Если он содержит обработчики событий или управляет состоянием.
import type React from "react"
interface StatCardProps {
title: string
subtitle: string
icon: React.ComponentType<{ size?: number; fill?: string; className?: string; style?: React.CSSProperties }>
fill:string
fill: string
onClick: () => void
}
@ -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>
{/* Текст */}

View File

@ -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>

View File

@ -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;
}

View File

@ -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="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>
);
};
)
}

View File

@ -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>

View File

@ -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,31 +400,10 @@ 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 />
</div>
</div>
)
}
}

View File

@ -8,7 +8,7 @@ export interface CourseExercise {
}
export interface Course {
ID: number
id: number
title: string
desc: string
url_file_img: string