исправлена нижняя навигация - переход на текущее упражнение/тренировка

This commit is contained in:
Tatyana 2025-09-08 21:01:13 +03:00
parent 4429713601
commit 90267942d7

View File

@ -1,32 +1,267 @@
"use client" "use client"
import type React from "react"; import type React from "react"
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom"
import { useState, useEffect } from "react"
import { connect } from "../confconnect"
import { getRouteHome } from "../shared/consts/router"; import { getRouteHome } from "../shared/consts/router"
import { getRouteCourses } from "../shared/consts/router"; import { getRouteCourses } from "../shared/consts/router"
import { getRouteExerciseByIndex } from "../shared/consts/router"; import { getRouteExerciseByIndex } from "../shared/consts/router"
import { getRouteSettings } from "../shared/consts/router"; import { getRouteSettings } from "../shared/consts/router"
import { HomeIcon } from "./icons/HomeIcon"; import { HomeIcon } from "./icons/HomeIcon"
import { CalendarIcon } from "./icons/CalendarIcon"; import { CalendarIcon } from "./icons/CalendarIcon"
import { DumbbellIcon } from "./icons/DumbbellIcon"; import { DumbbellIcon } from "./icons/DumbbellIcon"
import { SettingsIcon } from "./icons/SettingsIcon"; import { SettingsIcon } from "./icons/SettingsIcon"
import type { Course, User, CoursesApiResponse } from "../types/course"
interface CourseExercises {
id_exercise: number
day: number
position: number
title: string
desc: string
time: string
count: number
repeats: number
}
interface CurrentExercise {
courseId: string
exerciseId: string
exerciseIndex: number
day: number
title: string
position: number
isCompleted: boolean
}
const BottomNavigation: React.FC = () => { const BottomNavigation: React.FC = () => {
const history = useHistory() const history = useHistory()
const location = useLocation() const location = useLocation()
const [currentExercise, setCurrentExercise] = useState<CurrentExercise | null>(null)
const [courses, setCourses] = useState<Course[]>([])
const token = localStorage.getItem("authToken")
const getCurrentExercise = async (): Promise<CurrentExercise | null> => {
if (!token || courses.length === 0) return null
try {
let targetCourse: Course | null = null
console.log("[v0] BottomNav - Всего курсов:", courses.length)
console.log(
"[v0] BottomNav - Список курсов:",
courses.map((c) => ({ id: c.id, title: c.title })),
)
// Проходим по всем курсам и ищем незавершенный
for (const course of courses) {
console.log("[v0] BottomNav - Проверяем курс:", course.id, course.title)
// Загружаем упражнения для каждого курса
const response = await connect.get(`/pacient/${course.id}`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
const exercises = response.data.course_exercises || []
console.log("[v0] BottomNav - Упражнений в курсе", course.id, ":", exercises.length)
if (exercises.length === 0) continue // Пропускаем курсы без упражнений
// Проверяем, есть ли незавершенные упражнения в этом курсе
let hasIncompleteExercises = false
let completedCount = 0
for (const exercise of exercises) {
const storageKey = `exerciseProgress_${course.id}_${exercise.id_exercise}_day_${exercise.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) {
completedCount++
} else {
hasIncompleteExercises = true
}
}
console.log(
"[v0] BottomNav - Курс",
course.id,
"- завершено упражнений:",
completedCount,
"из",
exercises.length,
)
console.log("[v0] BottomNav - Курс", course.id, "- есть незавершенные:", hasIncompleteExercises)
// Если в курсе есть незавершенные упражнения, выбираем его
if (hasIncompleteExercises) {
targetCourse = course
console.log("[v0] BottomNav - Выбран курс как незавершенный:", course.id, course.title)
break // Берем первый найденный незавершенный курс
}
}
// Если все курсы завершены, берем последний курс
if (!targetCourse && courses.length > 0) {
targetCourse = courses[courses.length - 1]
console.log("[v0] BottomNav - Все курсы завершены, берем последний:", targetCourse.id, targetCourse.title)
}
if (!targetCourse) {
console.log("[v0] BottomNav - Не найден подходящий курс")
return null
}
console.log("[v0] BottomNav - Итоговый выбранный курс:", targetCourse.id, targetCourse.title)
// Загружаем упражнения выбранного курса
const response = await connect.get(`/pacient/${targetCourse.id}`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
const exercises = response.data.course_exercises || []
if (exercises.length === 0) {
return null
}
// Группируем упражнения по дням
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_${targetCourse.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: targetCourse.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: targetCourse.id.toString(),
exerciseId: lastExercise.id_exercise.toString(),
exerciseIndex: exercises.length - 1,
day: lastExercise.day,
title: lastExercise.title,
position: lastExercise.position,
isCompleted: true,
}
}
return foundExercise
} catch (error) {
console.error("[v0] BottomNav - Ошибка при получении текущего упражнения:", error)
return null
}
}
useEffect(() => {
if (!token) return
connect
.get<CoursesApiResponse>("/pacient/courses")
.then((response) => {
const users = response.data.courses || []
const allCourses: Course[] = []
users.forEach((user: User) => {
if (user.Courses && Array.isArray(user.Courses)) {
user.Courses.forEach((course) => {
allCourses.push({
id: course.id,
title: course.title,
desc: course.desc,
url_file_img: course.url_file_img,
course_exercises: course.course_exercises,
})
})
}
})
setCourses(allCourses)
})
.catch((error) => {
console.error("[v0] BottomNav - Ошибка при загрузке курсов:", error)
})
}, [token])
useEffect(() => {
if (courses.length > 0) {
getCurrentExercise().then(setCurrentExercise)
}
}, [courses])
const getWorkoutPath = () => {
if (currentExercise) {
return getRouteExerciseByIndex(currentExercise.courseId, currentExercise.exerciseIndex, currentExercise.day)
}
return getRouteExerciseByIndex(1, 0, 1)
}
const navItems = [ const navItems = [
{ path: getRouteHome(), icon: HomeIcon, label: "Домой" }, { path: getRouteHome(), icon: HomeIcon, label: "Домой" },
{ path: getRouteCourses(), icon: CalendarIcon, label: "Курсы" }, { path: getRouteCourses(), icon: CalendarIcon, label: "Курсы" },
{ path: getRouteExerciseByIndex(1,1,1), icon: DumbbellIcon, label: "Тренировка" }, { path: getWorkoutPath(), icon: DumbbellIcon, label: "Тренировка" },
{ path: getRouteSettings(), icon: SettingsIcon, label: "Меню" }, { path: getRouteSettings(), icon: SettingsIcon, label: "Меню" },
] ]
const isActive = (path: string) => { const isActive = (path: string) => {
// Проверка на совпадение или включение
return location.pathname === path || location.pathname.startsWith(path) return location.pathname === path || location.pathname.startsWith(path)
} }
@ -42,13 +277,11 @@ const BottomNavigation: React.FC = () => {
onClick={() => history.push(item.path)} onClick={() => history.push(item.path)}
className="relative flex flex-col items-center justify-center w-24 h-24 overflow-hidden group focus:outline-none focus-visible:ring-1 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#0D212C]" className="relative flex flex-col items-center justify-center w-24 h-24 overflow-hidden group focus:outline-none focus-visible:ring-1 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#0D212C]"
> >
{/* Active state background (glassmorphism rectangle) */}
<div <div
className={`absolute rounded-2xl shadow-md transition-all duration-300 ease-out bg-white/60 backdrop-blur-md border border-white/20 ${ className={`absolute rounded-2xl shadow-md transition-all duration-300 ease-out bg-white/60 backdrop-blur-md border border-white/20 ${
active ? "opacity-100 scale-100 w-20 h-20 top-2" : "opacity-0 scale-0 w-0 h-0 top-0" active ? "opacity-100 scale-100 w-20 h-20 top-2" : "opacity-0 scale-0 w-0 h-0 top-0"
}`} }`}
/> />
{/* Icon and Label container */}
<div <div
className={`relative z-10 flex flex-col items-center justify-center transition-all duration-300 ease-out ${ className={`relative z-10 flex flex-col items-center justify-center transition-all duration-300 ease-out ${
active ? "text-[#145058]" : "text-white/70 translate-y-0 group-hover:text-white" active ? "text-[#145058]" : "text-white/70 translate-y-0 group-hover:text-white"
@ -60,13 +293,11 @@ const BottomNavigation: React.FC = () => {
<span className="text-xs font-medium">{item.label}</span> <span className="text-xs font-medium">{item.label}</span>
<span className="sr-only">{item.label}</span> <span className="sr-only">{item.label}</span>
</div> </div>
{/* Bottom circle with glow */}
<div <div
className={`absolute w-4 h-4 rounded-full border-1 border-cyan-900 shadow-lg shadow-[#2BACBE]/50 animate-pulse transition-all duration-300 ease-out ${ className={`absolute w-4 h-4 rounded-full border-1 border-cyan-900 shadow-lg shadow-[#2BACBE]/50 animate-pulse transition-all duration-300 ease-out ${
active ? "opacity-100 scale-100 translate-y-[40px]" : "opacity-0 scale-0" active ? "opacity-100 scale-100 translate-y-[40px]" : "opacity-0 scale-0"
}`} }`}
> >
{/* Внутренний маленький круг */}
<div className="w-3 h-3 bg-cyan-900 rounded-full mt-[1px] ml-[1px]" /> <div className="w-3 h-3 bg-cyan-900 rounded-full mt-[1px] ml-[1px]" />
</div> </div>
</button> </button>