исправлена нижняя навигация - переход на текущее упражнение/тренировка
This commit is contained in:
parent
4429713601
commit
90267942d7
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user