новый дизайн_утвержденный по макету
This commit is contained in:
parent
694b053a10
commit
28d20b23a0
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Реабилитация</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
@ -10,16 +10,11 @@ import "@ionic/react/css/core.css"
|
||||
const App: React.FC = () => (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<div className="flex flex-col h-screen bg-gradient-to-br from-slate-100 via-teal-50 to-cyan-50">
|
||||
|
||||
{/* Контент с отступом равным высоте Header */}
|
||||
<div className="flex-1 overflow-auto pt-16 pb-16">
|
||||
<IonRouterOutlet>
|
||||
<AppRoutes />
|
||||
</IonRouterOutlet>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
)
|
||||
|
@ -3,14 +3,26 @@ import Home from "./pages/Home"
|
||||
import Login from "./pages/Login"
|
||||
import Register from "./pages/Register"
|
||||
import Welcome from "./pages/Welcome"
|
||||
import ForgotPassword from "./pages/ForgotPassword"
|
||||
import Courses from "./pages/Courses"
|
||||
import CourseExercises from "./pages/CourseExercises"
|
||||
import Exercise from "./pages/Exercise"
|
||||
import Settings from "./pages/Settings"
|
||||
import CourseComplete from "./pages/CourseComplete"
|
||||
|
||||
const AppRoutes = () => (
|
||||
<> <Route exact path="/" component={Welcome} />
|
||||
<>
|
||||
<Route exact path="/" component={Welcome} />
|
||||
<Route path="/home" component={Home} exact />
|
||||
<Route path="/register" component={Register} />
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/welcome" component={Welcome} />
|
||||
\
|
||||
<Route path="/forgot-password" component={ForgotPassword} />
|
||||
<Route path="/welcome" component={Welcome} />
|
||||
<Route path="/courses" component={Courses} />
|
||||
<Route path="/course/:id/exercises" component={CourseExercises} />
|
||||
<Route path="/exercise/:id" component={Exercise} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/course-complete" component={CourseComplete} />
|
||||
</>
|
||||
)
|
||||
|
||||
|
BIN
src/assets/emblem.png
Normal file
BIN
src/assets/emblem.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
3
src/assets/man.svg
Normal file
3
src/assets/man.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="231" height="414" viewBox="0 0 231 414" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M52.6304 288.512L34.3361 320.212C31.8969 324.5 28.1648 327.336 23.14 328.72C18.1152 330.105 13.4623 329.566 9.18144 327.103C4.90057 324.641 2.08324 320.892 0.729464 315.857C-0.624315 310.822 -0.0937818 306.148 2.32107 301.835L87.3896 154.363C75.8032 142.419 67.1134 128.789 61.3202 113.475C55.527 98.1616 52.6304 82.3884 52.6304 66.1557C52.6304 58.1925 53.3195 50.1558 54.6977 42.0456C56.0759 33.9354 58.4358 26.0457 61.7776 18.3766C63.607 13.7824 66.961 10.7932 71.8395 9.40881C76.7179 8.02444 81.2915 8.56349 85.5602 11.026C89.8289 13.4884 92.6523 17.1637 94.0304 22.0519C95.4086 26.9401 95.3294 31.8405 93.7926 36.7532C92.2681 41.3473 91.1278 46.0211 90.3716 50.7745C89.6154 55.5279 89.2312 60.3487 89.2191 65.2369C89.2191 81.4695 93.1828 96.7099 101.11 110.958C109.038 125.206 120.319 136.612 134.955 145.175L176.117 169.065C195.021 180.091 208.894 195.943 217.737 216.623C226.579 237.303 231 256.672 231 274.73C231 282.999 230.238 291.195 228.713 299.318C227.189 307.44 224.902 315.324 221.853 322.969C220.023 327.869 216.669 331.011 211.791 332.396C206.912 333.78 202.186 333.241 197.613 330.779C193.344 328.328 190.448 324.653 188.923 319.753C187.399 314.852 187.399 309.799 188.923 304.592C190.448 299.692 191.594 294.865 192.362 290.111C193.131 285.358 193.509 280.537 193.497 275.649C193.497 265.848 192.125 256.353 189.38 247.165C186.636 237.977 182.215 229.401 176.117 221.438L70.01 404.744C67.5708 409.032 63.8388 411.868 58.8139 413.253C53.7891 414.637 49.1362 414.098 44.8553 411.636C40.5745 409.173 37.7572 405.424 36.4034 400.389C35.0496 395.354 35.5801 390.68 37.995 386.368L83.7308 306.889L52.6304 288.512ZM185.264 147.013C175.202 147.013 166.592 143.417 159.433 136.226C152.273 129.034 148.688 120.379 148.676 110.26C148.663 100.14 152.249 91.4909 159.433 84.3118C166.616 77.1327 175.227 73.5308 185.264 73.5063C195.302 73.4818 203.918 77.0837 211.114 84.3118C218.31 91.5399 221.889 100.189 221.853 110.26C221.816 120.33 218.237 128.985 211.114 136.226C203.991 143.466 195.375 147.062 185.264 147.013ZM139.528 55.1298C131.601 55.1298 125.045 52.3733 119.862 46.8603C114.679 41.3473 112.087 34.9155 112.087 27.5649C112.087 19.6017 114.831 13.0167 120.319 7.81005C125.808 2.60335 132.211 0 139.528 0C147.456 0 154.011 2.75649 159.195 8.26946C164.378 13.7824 166.97 20.2142 166.97 27.5649C166.97 35.5281 164.226 42.113 158.737 47.3197C153.249 52.5264 146.846 55.1298 139.528 55.1298Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
114
src/components/BottomNavigation.tsx
Normal file
114
src/components/BottomNavigation.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useHistory, useLocation } from "react-router-dom"
|
||||
|
||||
const BottomNavigation: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
const HomeIcon = ({ active }: { active: boolean }) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className={active ? "text-white" : "text-[#5F5C5C]"}>
|
||||
<path
|
||||
d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M9 22V12H15V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CoursesIcon = ({ active }: { active: boolean }) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className={active ? "text-white" : "text-[#5F5C5C]"}>
|
||||
<path
|
||||
d="M2 3H8C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7V21C12 20.2044 11.6839 19.4413 11.1213 18.8787C10.5587 18.3161 9.79565 18 9 18H2V3Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M22 3H16C14.9391 3 13.9217 3.42143 13.1716 4.17157C12.4214 4.92172 12 5.93913 12 7V21C12 20.2044 12.3161 19.4413 12.8787 18.8787C13.4413 18.3161 14.2044 18 15 18H22V3Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ExerciseIcon = ({ active }: { active: boolean }) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className={active ? "text-white" : "text-[#5F5C5C]"}>
|
||||
<path
|
||||
d="M6.2 5H12.8C13.9201 5 14.9798 5.44772 15.7549 6.22183C16.5301 6.99594 16.9778 8.05556 16.9778 9.17647V19.8235C16.9778 18.7026 16.5301 17.643 15.7549 16.8689C14.9798 16.0948 13.9201 15.6471 12.8 15.6471H6.2V5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.2 5C5.07989 5 4.02015 5.44772 3.24505 6.22183C2.46995 6.99594 2.02222 8.05556 2.02222 9.17647V19.8235C2.02222 18.7026 2.46995 17.643 3.24505 16.8689C4.02015 16.0948 5.07989 15.6471 6.2 15.6471"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SettingsIcon = ({ active }: { active: boolean }) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className={active ? "text-white" : "text-[#5F5C5C]"}>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M19.4 15C19.2669 15.3016 19.2272 15.6362 19.286 15.9606C19.3448 16.285 19.4995 16.5843 19.73 16.82L19.79 16.88C19.976 17.0657 20.1235 17.2863 20.2241 17.5291C20.3248 17.7719 20.3766 18.0322 20.3766 18.295C20.3766 18.5578 20.3248 18.8181 20.2241 19.0609C20.1235 19.3037 19.976 19.5243 19.79 19.71C19.6043 19.896 19.3837 20.0435 19.1409 20.1441C18.8981 20.2448 18.6378 20.2966 18.375 20.2966C18.1122 20.2966 17.8519 20.2448 17.6091 20.1441C17.3663 20.0435 17.1457 19.896 16.96 19.71L16.9 19.65C16.6643 19.4195 16.365 19.2648 16.0406 19.206C15.7162 19.1472 15.3816 19.1869 15.08 19.32C14.7842 19.4468 14.532 19.6572 14.3543 19.9255C14.1766 20.1938 14.0813 20.5082 14.08 20.83V21C14.08 21.5304 13.8693 22.0391 13.4942 22.4142C13.1191 22.7893 12.6104 23 12.08 23C11.5496 23 11.0409 22.7893 10.6658 22.4142C10.2907 22.0391 10.08 21.5304 10.08 21V20.91C10.0723 20.579 9.96512 20.2569 9.77251 19.9859C9.5799 19.7148 9.31074 19.5063 9 19.38C8.69838 19.2469 8.36381 19.2072 8.03941 19.266C7.71502 19.3248 7.41568 19.4795 7.18 19.71L7.12 19.77C6.93425 19.956 6.71368 20.1035 6.47088 20.2041C6.22808 20.3048 5.96783 20.3566 5.705 20.3566C5.44217 20.3566 5.18192 20.3048 4.93912 20.2041C4.69632 20.1035 4.47575 19.956 4.29 19.77C4.10405 19.5843 3.95653 19.3637 3.85588 19.1209C3.75523 18.8781 3.70343 18.6178 3.70343 18.355C3.70343 18.0922 3.75523 17.8319 3.85588 17.5891C3.95653 17.3463 4.10405 17.1257 4.29 16.94L4.35 16.88C4.58054 16.6443 4.73519 16.345 4.794 16.0206C4.85282 15.6962 4.81312 15.3616 4.68 15.06C4.55324 14.7642 4.34276 14.512 4.07447 14.3343C3.80618 14.1566 3.49179 14.0613 3.17 14.06H3C2.46957 14.06 1.96086 13.8493 1.58579 13.4742C1.21071 13.0991 1 12.5904 1 12.06C1 11.5296 1.21071 11.0209 1.58579 10.6458C1.96086 10.2707 2.46957 10.06 3 10.06H3.09C3.42099 10.0523 3.742 9.94512 4.01309 9.75251C4.28417 9.5599 4.49268 9.29074 4.62 8.98C4.75312 8.67838 4.79282 8.34381 4.734 8.01941C4.67519 7.69502 4.52054 7.39568 4.29 7.16L4.23 7.1C4.04405 6.91425 3.89653 6.69368 3.79588 6.45088C3.69523 6.20808 3.64343 5.94783 3.64343 5.685C3.64343 5.42217 3.69523 5.16192 3.79588 4.91912C3.89653 4.67632 4.04405 4.45575 4.23 4.27C4.41575 4.08405 4.63632 3.93653 4.87912 3.83588C5.12192 3.73523 5.38217 3.68343 5.645 3.68343C5.90783 3.68343 6.16808 3.73523 6.41088 3.83588C6.65368 3.93653 6.87425 4.08405 7.06 4.27L7.12 4.33C7.35568 4.56054 7.65502 4.71519 7.97941 4.774C8.30381 4.83282 8.63838 4.79312 8.94 4.66H9C9.29577 4.53324 9.54802 4.32276 9.72569 4.05447C9.90337 3.78618 9.99872 3.47179 10 3.15V3C10 2.46957 10.2107 1.96086 10.5858 1.58579C10.9609 1.21071 11.4696 1 12 1C12.5304 1 13.0391 1.21071 13.4142 1.58579C13.7893 1.96086 14 2.46957 14 3V3.09C14.0013 3.41179 14.0966 3.72618 14.2743 3.99447C14.452 4.26276 14.7042 4.47324 15 4.6C15.3016 4.73312 15.6362 4.77282 15.9606 4.714C16.285 4.65519 16.5843 4.50054 16.82 4.27L16.88 4.21C17.0657 4.02405 17.2863 3.87653 17.5291 3.77588C17.7719 3.67523 18.0322 3.62343 18.295 3.62343C18.5578 3.62343 18.8181 3.67523 19.0609 3.77588C19.3037 3.87653 19.5243 4.02405 19.71 4.21C19.896 4.39575 20.0435 4.61632 20.1441 4.85912C20.2448 5.10192 20.2966 5.36217 20.2966 5.625C20.2966 5.88783 20.2448 6.14808 20.1441 6.39088C20.0435 6.63368 19.896 6.85425 19.71 7.04L19.65 7.1C19.4195 7.33568 19.2648 7.63502 19.206 7.95941C19.1472 8.28381 19.1869 8.61838 19.32 8.92V9C19.4468 9.29577 19.6572 9.54802 19.9255 9.72569C20.1938 9.90337 20.5082 9.99872 20.83 10H21C21.5304 10 22.0391 10.2107 22.4142 10.5858C22.7893 10.9609 23 11.4696 23 12C23 12.5304 22.7893 13.0391 22.4142 13.4142C22.0391 13.7893 21.5304 14 21 14H20.91C20.5882 14.0013 20.2738 14.0966 20.0055 14.2743C19.7372 14.452 19.5268 14.7042 19.4 15Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const navItems = [
|
||||
{ path: "/home", icon: HomeIcon, label: "Домой" },
|
||||
{ path: "/courses", icon: CoursesIcon, label: "Курсы" },
|
||||
{ path: "/exercise/1", icon: ExerciseIcon, label: "Заниматься" },
|
||||
{ path: "/settings", icon: SettingsIcon, label: "Меню" },
|
||||
]
|
||||
|
||||
const isActive = (path: string) => {
|
||||
if (path === "/exercise/1") return location.pathname.includes("/exercise")
|
||||
return location.pathname === path
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white/20 backdrop-blur-xl border-t border-white/10 px-4 py-3 z-50 shadow-2xl">
|
||||
<div className="flex justify-around items-center max-w-md mx-auto">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.path)
|
||||
const IconComponent = item.icon
|
||||
return (
|
||||
<button
|
||||
key={item.path}
|
||||
onClick={() => history.push(item.path)}
|
||||
className={`flex flex-col items-center py-2 px-4 rounded-2xl transition-all duration-300 transform ${
|
||||
active
|
||||
? "text-white bg-[#2BACBE]/80 backdrop-blur-lg scale-110 shadow-lg"
|
||||
: "text-[#5F5C5C] hover:text-[#2BACBE] hover:bg-white/10 hover:scale-105"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1 filter drop-shadow-sm">
|
||||
<IconComponent active={active} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold tracking-wide">{item.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomNavigation
|
@ -1,154 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
import { IonCard, IonCardContent, IonButton, IonIcon, IonBadge } from "@ionic/react"
|
||||
import { playOutline, videocamOutline, closeCircleOutline } from "ionicons/icons" // Добавлен videocamOutline и closeCircleOutline
|
||||
|
||||
interface Exercise {
|
||||
id: number
|
||||
name: string
|
||||
duration: number // в секундах
|
||||
description: string
|
||||
difficulty: "easy" | "medium" | "hard"
|
||||
completed: boolean
|
||||
videoUrl?: string // Добавлено поле для URL видео
|
||||
}
|
||||
|
||||
interface ExerciseItemProps {
|
||||
exercise: Exercise
|
||||
onStart: (exercise: Exercise) => void
|
||||
isCurrent: boolean
|
||||
isDisabled: boolean
|
||||
onVideoPlayToggle: (isPlaying: boolean) => void // Добавлено для уведомления родителя
|
||||
}
|
||||
|
||||
const ExerciseItem: React.FC<ExerciseItemProps> = ({ exercise, onStart, isCurrent, isDisabled, onVideoPlayToggle }) => {
|
||||
const [showVideo, setShowVideo] = useState(false)
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case "easy":
|
||||
return "text-emerald-400"
|
||||
case "medium":
|
||||
return "text-yellow-400"
|
||||
case "hard":
|
||||
return "text-red-400"
|
||||
default:
|
||||
return "text-slate-400"
|
||||
}
|
||||
}
|
||||
|
||||
const getDifficultyBg = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case "easy":
|
||||
return "bg-emerald-500/20 border-emerald-500/30"
|
||||
case "medium":
|
||||
return "bg-yellow-500/20 border-yellow-500/30"
|
||||
case "hard":
|
||||
return "bg-red-500/20 border-red-500/30"
|
||||
default:
|
||||
return "bg-slate-500/20 border-slate-500/30"
|
||||
}
|
||||
}
|
||||
|
||||
const handleVideoToggle = () => {
|
||||
setShowVideo((prev) => {
|
||||
onVideoPlayToggle(!prev) // Уведомляем родителя об изменении состояния видео
|
||||
return !prev
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<IonCard
|
||||
className={`${exercise.completed ? "bg-emerald-50/80" : "bg-white/80"} backdrop-blur-sm border border-teal-200/50 shadow-md`}
|
||||
>
|
||||
<IonCardContent>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${exercise.completed ? "text-emerald-700" : "text-slate-800"}`}>
|
||||
{exercise.name}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600 mt-1">{exercise.description}</p>
|
||||
</div>
|
||||
{exercise.completed && (
|
||||
<IonBadge color="success" className="ml-2">
|
||||
Выполнено
|
||||
</IonBadge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showVideo && exercise.videoUrl && (
|
||||
<div className="mb-4 relative">
|
||||
<video
|
||||
src={exercise.videoUrl}
|
||||
controls
|
||||
poster={`/placeholder.svg?height=200&width=300&query=${encodeURIComponent(exercise.name)}`}
|
||||
className="w-full rounded-lg"
|
||||
>
|
||||
Ваш браузер не поддерживает видео тег.
|
||||
</video>
|
||||
<IonButton
|
||||
fill="clear"
|
||||
size="small"
|
||||
onClick={handleVideoToggle}
|
||||
className="absolute top-2 right-2 text-white bg-black/50 rounded-full"
|
||||
>
|
||||
<IonIcon icon={closeCircleOutline} />
|
||||
</IonButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-slate-500">{formatTime(exercise.duration)}</span>
|
||||
<IonBadge
|
||||
className={`${getDifficultyBg(exercise.difficulty)} ${getDifficultyColor(exercise.difficulty)} border`}
|
||||
>
|
||||
{exercise.difficulty === "easy" ? "Легко" : exercise.difficulty === "medium" ? "Средне" : "Сложно"}
|
||||
</IonBadge>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{exercise.videoUrl && (
|
||||
<IonButton
|
||||
size="small"
|
||||
onClick={handleVideoToggle}
|
||||
className="bg-gradient-to-r from-blue-500 to-indigo-500"
|
||||
disabled={isCurrent} // Отключаем кнопку видео, если упражнение активно
|
||||
>
|
||||
<IonIcon icon={videocamOutline} slot="start" />
|
||||
{showVideo ? "Скрыть" : "Видео"}
|
||||
</IonButton>
|
||||
)}
|
||||
|
||||
{!exercise.completed && !isCurrent && (
|
||||
<IonButton
|
||||
size="small"
|
||||
onClick={() => onStart(exercise)}
|
||||
className="bg-gradient-to-r from-teal-500 to-cyan-500"
|
||||
disabled={isDisabled || showVideo} // Отключаем кнопку, если другое упражнение активно или видео проигрывается
|
||||
>
|
||||
<IonIcon icon={playOutline} slot="start" />
|
||||
Начать
|
||||
</IonButton>
|
||||
)}
|
||||
{isCurrent && (
|
||||
<IonBadge color="primary" className="ml-2 animate-pulse">
|
||||
Активно
|
||||
</IonBadge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExerciseItem
|
@ -1,176 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
type TabType = "home" | "courses" | "profile"
|
||||
|
||||
interface FooterProps {
|
||||
onTabChange?: (tab: TabType) => void
|
||||
activeTab?: TabType
|
||||
}
|
||||
|
||||
// Строгие и аккуратные SVG иконки
|
||||
const HomeIcon = ({ filled = false, className = "" }) => (
|
||||
<svg
|
||||
className={className}
|
||||
fill={filled ? "currentColor" : "none"}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={filled ? 0 : 1.5}
|
||||
>
|
||||
{filled ? (
|
||||
<path d="M11.47 3.841a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.061l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.689z" />
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
|
||||
const BookIcon = ({ filled = false, className = "" }) => (
|
||||
<svg
|
||||
className={className}
|
||||
fill={filled ? "currentColor" : "none"}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={filled ? 0 : 1.5}
|
||||
>
|
||||
{filled ? (
|
||||
<path d="M11.25 4.533A9.707 9.707 0 006 3a9.735 9.735 0 00-3.25.555.75.75 0 00-.5.707v14.25a.75.75 0 001 .707A8.237 8.237 0 016 18.75c1.995 0 3.823.707 5.25 1.886V4.533zM12.75 20.636A8.214 8.214 0 0118 18.75c.966 0 1.89.166 2.75.47a.75.75 0 001-.708V4.262a.75.75 0 00-.5-.707A9.735 9.735 0 0018 3a9.707 9.707 0 00-5.25 1.533v16.103z" />
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6.042A9.02 9.02 0 016 3.75c-1.052 0-2.062.18-3 .512v14.25A8.998 8.998 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a9.02 9.02 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.998 8.998 0 0018 18a8.998 8.998 0 00-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
|
||||
const UserIcon = ({ filled = false, className = "" }) => (
|
||||
<svg
|
||||
className={className}
|
||||
fill={filled ? "currentColor" : "none"}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={filled ? 0 : 1.5}
|
||||
>
|
||||
{filled ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
|
||||
function Footer({ onTabChange, activeTab: controlledActiveTab }: FooterProps) {
|
||||
const [internalActiveTab, setInternalActiveTab] = useState<TabType>("home")
|
||||
|
||||
const activeTab = controlledActiveTab || internalActiveTab
|
||||
|
||||
const handleTabChange = (tab: TabType) => {
|
||||
if (!controlledActiveTab) {
|
||||
setInternalActiveTab(tab)
|
||||
}
|
||||
onTabChange?.(tab)
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: "home" as TabType,
|
||||
label: "Главная",
|
||||
icon: HomeIcon,
|
||||
},
|
||||
{
|
||||
id: "courses" as TabType,
|
||||
label: "Курсы",
|
||||
icon: BookIcon,
|
||||
},
|
||||
{
|
||||
id: "profile" as TabType,
|
||||
label: "Профиль",
|
||||
icon: UserIcon,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50">
|
||||
{/* Основной контейнер */}
|
||||
<div className="relative">
|
||||
{/* Фон с размытием */}
|
||||
<div className="absolute inset-0 bg-slate-900/95 backdrop-blur-2xl border-t border-teal-700/30" />
|
||||
|
||||
{/* Тонкая верхняя линия */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/60 to-transparent" />
|
||||
|
||||
{/* Контент */}
|
||||
<div className="relative px-8 py-4">
|
||||
<div className="flex justify-center items-center max-w-sm mx-auto">
|
||||
<div className="flex space-x-12">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id
|
||||
const Icon = tab.icon
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
className={`
|
||||
group relative flex flex-col items-center justify-center
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-none focus:ring-2 focus:ring-teal-400/50 focus:ring-offset-2 focus:ring-offset-slate-900
|
||||
rounded-lg p-2 min-w-[56px]
|
||||
${isActive ? "" : "hover:bg-teal-900/20"}
|
||||
`}
|
||||
>
|
||||
{/* Активный индикатор */}
|
||||
{isActive && (
|
||||
<div className="absolute -top-1 left-1/2 transform -translate-x-1/2 w-8 h-0.5 bg-gradient-to-r from-teal-400 to-cyan-400 rounded-full" />
|
||||
)}
|
||||
|
||||
{/* Иконка */}
|
||||
<div className="mb-1">
|
||||
<Icon
|
||||
filled={isActive}
|
||||
className={`
|
||||
w-6 h-6 transition-colors duration-200
|
||||
${isActive ? "text-teal-300" : "text-slate-400 group-hover:text-teal-400"}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Подпись */}
|
||||
<span
|
||||
className={`
|
||||
text-xs font-medium transition-colors duration-200
|
||||
${isActive ? "text-teal-200" : "text-slate-500 group-hover:text-teal-300"}
|
||||
`}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Нижний отступ для мобильных устройств */}
|
||||
<div className="h-2 bg-slate-900/95 sm:h-0" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
@ -1,40 +0,0 @@
|
||||
"use client"
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 shadow-lg backdrop-blur-md bg-slate-900/80 border-b border-teal-700/30 transition-all duration-300">
|
||||
<div className="relative max-w-7xl mx-auto px-6 py-4">
|
||||
{/* Верхняя граница с градиентом */}
|
||||
<div className="absolute inset-0 bg-slate-900/95 backdrop-blur-2xl border-b border-teal-700/30 rounded-lg" />
|
||||
|
||||
{/* Нижняя граница с градиентом */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/60 to-transparent" />
|
||||
|
||||
{/* Контент */}
|
||||
<div className="flex items-center justify-between relative z-10">
|
||||
{/* Логотип и название */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Иконка с градиентом */}
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-teal-600 to-cyan-600 rounded-xl flex items-center justify-center shadow-lg shadow-teal-500/20 transition-transform hover:scale-105 hover:shadow-xl">
|
||||
<svg
|
||||
className="w-6 h-6 text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* Название и описание */}
|
||||
<div className="flex flex-col font-semibold text-white">
|
||||
<h1 className="text-xl bg-gradient-to-r from-teal-300 to-cyan-300 bg-clip-text text-transparent leading-tight tracking-wide transition-all duration-300 hover:scale-105 hover:text-cyan-200">
|
||||
МедРеабилитация
|
||||
</h1>
|
||||
<p className="text-xs text-teal-400/80 mt-px">Центр восстановления</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Header
|
490
src/index.css
490
src/index.css
@ -1,207 +1,361 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
@keyframes gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
.animate-gradient {
|
||||
background-size: 300% 300%;
|
||||
animation: gradient 10s ease infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Ionic переменные для кастомизации */
|
||||
:root {
|
||||
/* Основные цвета приложения */
|
||||
--ion-color-primary: #0f766e;
|
||||
--ion-color-primary-rgb: 15, 118, 110;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #0d6660;
|
||||
--ion-color-primary-tint: #27847e;
|
||||
|
||||
/* Вторичные цвета */
|
||||
--ion-color-secondary: #06b6d4;
|
||||
--ion-color-secondary-rgb: 6, 182, 212;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #05a0ba;
|
||||
--ion-color-secondary-tint: #1fc1d8;
|
||||
|
||||
/* Цвет успеха */
|
||||
--ion-color-success: #10b981;
|
||||
--ion-color-success-rgb: 16, 185, 129;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #0ea372;
|
||||
--ion-color-success-tint: #28c78e;
|
||||
|
||||
/* Цвет предупреждения */
|
||||
--ion-color-warning: #f59e0b;
|
||||
--ion-color-warning-rgb: 245, 158, 11;
|
||||
--ion-color-warning-contrast: #ffffff;
|
||||
--ion-color-warning-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-warning-shade: #d8890a;
|
||||
--ion-color-warning-tint: #f6a824;
|
||||
|
||||
/* Цвет опасности */
|
||||
--ion-color-danger: #ef4444;
|
||||
--ion-color-danger-rgb: 239, 68, 68;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #d23c3c;
|
||||
--ion-color-danger-tint: #f15757;
|
||||
|
||||
/* Фон приложения */
|
||||
--ion-background-color: #f8fafc;
|
||||
--ion-background-color-rgb: 248, 250, 252;
|
||||
|
||||
/* Цвет текста */
|
||||
--ion-text-color: #1e293b;
|
||||
--ion-text-color-rgb: 30, 41, 59;
|
||||
|
||||
/* Цвета для карточек */
|
||||
--ion-card-background: rgba(255, 255, 255, 0.9);
|
||||
--ion-item-background: rgba(255, 255, 255, 0.8);
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
/* Кастомные стили для медицинского приложения */
|
||||
.medical-gradient {
|
||||
background: linear-gradient(135deg, #0f766e 0%, #06b6d4 100%);
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
.medical-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(15, 118, 110, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(15, 118, 110, 0.1);
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
.exercise-timer {
|
||||
background: linear-gradient(135deg, #0f766e, #06b6d4);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 1rem;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Анимации для таймера */
|
||||
@keyframes pulse-medical {
|
||||
@layer components {
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
}
|
||||
|
||||
.glass-morphism-dark {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
.scrollable-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(43, 172, 190, 0.3) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.animate-bounce-gentle {
|
||||
animation: bounceGentle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-glow {
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translateY(-10px) rotate(1deg);
|
||||
}
|
||||
66% {
|
||||
transform: translateY(-5px) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceGentle {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.timer-pulse {
|
||||
animation: pulse-medical 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Стили для прогресс-бара */
|
||||
.progress-medical {
|
||||
background: linear-gradient(90deg, #10b981, #06b6d4);
|
||||
border-radius: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Кастомные стили для Ionic компонентов */
|
||||
ion-card.medical-card {
|
||||
--background: rgba(255, 255, 255, 0.9);
|
||||
--color: #1e293b;
|
||||
border: 1px solid rgba(15, 118, 110, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
|
||||
ion-button.medical-button {
|
||||
--background: linear-gradient(135deg, #0f766e, #06b6d4);
|
||||
--color: white;
|
||||
--border-radius: 12px;
|
||||
--box-shadow: 0 4px 16px rgba(15, 118, 110, 0.3);
|
||||
}
|
||||
|
||||
ion-button.medical-button:hover {
|
||||
--box-shadow: 0 6px 20px rgba(15, 118, 110, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Стили для header и footer */
|
||||
ion-header.medical-header {
|
||||
--background: rgba(15, 23, 42, 0.95);
|
||||
--color: white;
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(15, 118, 110, 0.3);
|
||||
}
|
||||
|
||||
ion-footer.medical-footer {
|
||||
--background: rgba(15, 23, 42, 0.95);
|
||||
--color: white;
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid rgba(15, 118, 110, 0.3);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.medical-card {
|
||||
margin: 0.5rem;
|
||||
border-radius: 12px;
|
||||
@keyframes glow {
|
||||
from {
|
||||
box-shadow: 0 0 20px rgba(43, 172, 190, 0.3);
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 30px rgba(43, 172, 190, 0.6);
|
||||
}
|
||||
|
||||
.exercise-timer {
|
||||
padding: 0.75rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для статусов упражнений */
|
||||
.exercise-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.exercise-active {
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||||
animation: pulse-medical 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.exercise-pending {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
/* Улучшенные тени для глубины */
|
||||
.shadow-medical {
|
||||
box-shadow: 0 4px 6px -1px rgba(15, 118, 110, 0.1), 0 2px 4px -1px rgba(15, 118, 110, 0.06);
|
||||
}
|
||||
|
||||
.shadow-medical-lg {
|
||||
box-shadow: 0 10px 15px -3px rgba(15, 118, 110, 0.1), 0 4px 6px -2px rgba(15, 118, 110, 0.05);
|
||||
}
|
||||
|
||||
/* Кастомные скроллбары */
|
||||
/* Enhanced scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(15, 118, 110, 0.3);
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(45deg, #2bacbe, #5f5c5c);
|
||||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(15, 118, 110, 0.5);
|
||||
background: linear-gradient(45deg, #2bacbe, #2bacbe);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Smooth transitions for all interactive elements */
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
div {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
button:focus,
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: 2px solid #2bacbe;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading-spinner {
|
||||
border: 3px solid rgba(43, 172, 190, 0.1);
|
||||
border-top: 3px solid #2bacbe;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Backdrop blur support */
|
||||
@supports (backdrop-filter: blur(20px)) {
|
||||
.backdrop-blur-2xl {
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (backdrop-filter: blur(20px)) {
|
||||
.backdrop-blur-2xl {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modern gradient backgrounds */
|
||||
.gradient-mesh {
|
||||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced card hover effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Improved button styles */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 12px 24px;
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
@ -1,69 +0,0 @@
|
||||
import type React from "react"
|
||||
import { IonPage, IonContent, IonList, IonItem, IonLabel, IonIcon, IonToggle, IonButton } from "@ionic/react"
|
||||
import {
|
||||
personCircleOutline,
|
||||
mailOutline,
|
||||
lockClosedOutline,
|
||||
notificationsOutline,
|
||||
moonOutline,
|
||||
logOutOutline,
|
||||
} from "ionicons/icons"
|
||||
import Header from "../components/Header"
|
||||
import Footer from "../components/Footer"
|
||||
|
||||
const Account: React.FC = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<Header title="Account" />
|
||||
<IonContent fullscreen className="ion-padding bg-dark-app">
|
||||
<div className="px-4 py-4">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<img
|
||||
src="/placeholder.svg?height=100&width=100"
|
||||
alt="Profile"
|
||||
className="w-24 h-24 rounded-full object-cover mb-4 border-2 border-tertiary-app"
|
||||
/>
|
||||
<h2 className="text-2xl font-bold text-text-light-app">Himanshi Kashyap</h2>
|
||||
<p className="text-sm text-text-muted-app">himanshi.kashyap@example.com</p>
|
||||
</div>
|
||||
|
||||
<IonList className="bg-card-bg-app rounded-lg-app border border-border-app mb-8">
|
||||
<IonItem lines="none" className="rounded-t-lg-app">
|
||||
<IonIcon icon={personCircleOutline} slot="start" color="tertiary" />
|
||||
<IonLabel className="text-text-light-app">Edit Profile</IonLabel>
|
||||
</IonItem>
|
||||
<IonItem lines="none">
|
||||
<IonIcon icon={mailOutline} slot="start" color="tertiary" />
|
||||
<IonLabel className="text-text-light-app">Change Email</IonLabel>
|
||||
</IonItem>
|
||||
<IonItem lines="none" className="rounded-b-lg-app">
|
||||
<IonIcon icon={lockClosedOutline} slot="start" color="tertiary" />
|
||||
<IonLabel className="text-text-light-app">Change Password</IonLabel>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList className="bg-card-bg-app rounded-lg-app border border-border-app mb-8">
|
||||
<IonItem lines="none" className="rounded-t-lg-app">
|
||||
<IonIcon icon={notificationsOutline} slot="start" color="tertiary" />
|
||||
<IonLabel className="text-text-light-app">Notifications</IonLabel>
|
||||
<IonToggle slot="end" color="tertiary" checked={true} />
|
||||
</IonItem>
|
||||
<IonItem lines="none" className="rounded-b-lg-app">
|
||||
<IonIcon icon={moonOutline} slot="start" color="tertiary" />
|
||||
<IonLabel className="text-text-light-app">Dark Mode</IonLabel>
|
||||
<IonToggle slot="end" color="tertiary" checked={true} />
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonButton expand="block" fill="outline" color="danger" className="rounded-md-app">
|
||||
<IonIcon icon={logOutOutline} slot="start" />
|
||||
Log Out
|
||||
</IonButton>
|
||||
</div>
|
||||
</IonContent>
|
||||
<Footer />
|
||||
</IonPage>
|
||||
)
|
||||
}
|
||||
|
||||
export default Account
|
141
src/pages/CourseComplete.tsx
Normal file
141
src/pages/CourseComplete.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
|
||||
const CourseComplete: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const [animationPhase, setAnimationPhase] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const timer1 = setTimeout(() => setAnimationPhase(1), 500)
|
||||
const timer2 = setTimeout(() => setAnimationPhase(2), 1500)
|
||||
const timer3 = setTimeout(() => setAnimationPhase(3), 2500)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer1)
|
||||
clearTimeout(timer2)
|
||||
clearTimeout(timer3)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-400 via-blue-500 to-purple-600 flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* Background Animation */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-20 left-10 w-20 h-20 bg-white/10 rounded-full animate-pulse"></div>
|
||||
<div className="absolute bottom-32 right-16 w-16 h-16 bg-white/10 rounded-full animate-bounce"></div>
|
||||
<div className="absolute top-1/2 left-1/4 w-12 h-12 bg-white/10 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="text-center z-10 max-w-md">
|
||||
{/* Volleyball Animation */}
|
||||
<div className="relative mb-8 h-40">
|
||||
{/* Net */}
|
||||
<div
|
||||
className={`absolute bottom-0 left-1/2 transform -translate-x-1/2 transition-all duration-1000 ${
|
||||
animationPhase >= 1 ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div className="w-32 h-20 border-2 border-white/60 border-b-0">
|
||||
<div className="grid grid-cols-8 h-full">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="border-r border-white/40 last:border-r-0"></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-rows-4 h-full absolute inset-0">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="border-b border-white/40 last:border-b-0"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Volleyball */}
|
||||
<div
|
||||
className={`absolute transition-all duration-2000 ${
|
||||
animationPhase >= 2
|
||||
? "bottom-8 left-1/2 transform -translate-x-1/2 scale-75"
|
||||
: "bottom-32 left-8 scale-100"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-16 h-16 rounded-full bg-white flex items-center justify-center text-2xl shadow-lg ${
|
||||
animationPhase >= 2 ? "animate-bounce" : ""
|
||||
}`}
|
||||
>
|
||||
🏐
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success particles */}
|
||||
{animationPhase >= 3 && (
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-4 left-8 animate-ping">⭐</div>
|
||||
<div className="absolute top-8 right-12 animate-pulse">✨</div>
|
||||
<div className="absolute bottom-16 left-16 animate-bounce">🎉</div>
|
||||
<div className="absolute bottom-12 right-8 animate-ping">🏆</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Congratulations */}
|
||||
<div
|
||||
className={`transition-all duration-1000 delay-500 ${
|
||||
animationPhase >= 2 ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||
}`}
|
||||
>
|
||||
<h1 className="text-4xl font-bold text-white mb-4">Поздравляем! 🎉</h1>
|
||||
<p className="text-white/90 text-lg mb-2">Вы успешно завершили курс</p>
|
||||
<p className="text-white/80 text-xl font-semibold mb-6">"Восстановление колена"</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div
|
||||
className={`bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20 mb-8 transition-all duration-1000 delay-1000 ${
|
||||
animationPhase >= 3 ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">12</div>
|
||||
<div className="text-white/70 text-sm">Упражнений</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">4</div>
|
||||
<div className="text-white/70 text-sm">Недели</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">100%</div>
|
||||
<div className="text-white/70 text-sm">Завершено</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div
|
||||
className={`space-y-4 transition-all duration-1000 delay-1500 ${
|
||||
animationPhase >= 3 ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => history.push("/courses")}
|
||||
className="w-full bg-white/20 backdrop-blur-lg hover:bg-white/30 text-white font-semibold py-3 px-6 rounded-xl border border-white/30 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
Выбрать новый курс
|
||||
</button>
|
||||
<button
|
||||
onClick={() => history.push("/home")}
|
||||
className="w-full bg-transparent border-2 border-white/50 hover:bg-white/10 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200"
|
||||
>
|
||||
На главную
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CourseComplete
|
312
src/pages/CourseExercises.tsx
Normal file
312
src/pages/CourseExercises.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
import { useHistory, useParams } from "react-router-dom"
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
|
||||
const CourseExercises: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [currentSlide, setCurrentSlide] = useState(0)
|
||||
|
||||
const exercises = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Подъемы ног лежа",
|
||||
duration: "15 мин",
|
||||
sets: 3,
|
||||
reps: 12,
|
||||
image: "/placeholder.svg?height=200&width=300",
|
||||
difficulty: "Легкий",
|
||||
description: "Укрепление мышц бедра и улучшение подвижности коленного сустава",
|
||||
calories: 45,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Приседания у стены",
|
||||
duration: "10 мин",
|
||||
sets: 2,
|
||||
reps: 15,
|
||||
image: "/placeholder.svg?height=200&width=300",
|
||||
difficulty: "Средний",
|
||||
description: "Безопасные приседания для восстановления силы ног",
|
||||
calories: 60,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Растяжка квадрицепса",
|
||||
duration: "8 мин",
|
||||
sets: 1,
|
||||
reps: 30,
|
||||
image: "/placeholder.svg?height=200&width=300",
|
||||
difficulty: "Легкий",
|
||||
description: "Улучшение гибкости и снятие напряжения",
|
||||
calories: 25,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Укрепление икр",
|
||||
duration: "12 мин",
|
||||
sets: 3,
|
||||
reps: 20,
|
||||
image: "/placeholder.svg?height=200&width=300",
|
||||
difficulty: "Средний",
|
||||
description: "Развитие силы и выносливости икроножных мышц",
|
||||
calories: 40,
|
||||
},
|
||||
]
|
||||
|
||||
const nextSlide = () => {
|
||||
setCurrentSlide((prev) => (prev + 1) % exercises.length)
|
||||
}
|
||||
|
||||
const prevSlide = () => {
|
||||
setCurrentSlide((prev) => (prev - 1 + exercises.length) % exercises.length)
|
||||
}
|
||||
|
||||
const currentExercise = exercises[currentSlide]
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case "Легкий":
|
||||
return "bg-gradient-to-r from-emerald-400 to-green-500"
|
||||
case "Средний":
|
||||
return "bg-gradient-to-r from-amber-400 to-orange-500"
|
||||
case "Сложный":
|
||||
return "bg-gradient-to-r from-red-400 to-pink-500"
|
||||
default:
|
||||
return "bg-gradient-to-r from-gray-400 to-gray-500"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 overflow-y-auto scrollable-content">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-40 bg-gradient-to-br from-[#2BACBE] via-blue-500 to-indigo-600 backdrop-blur-xl">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/10 to-transparent"></div>
|
||||
<div className="relative px-4 sm:px-6 pt-12 pb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<button
|
||||
onClick={() => history.goBack()}
|
||||
className="w-12 h-12 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center border border-white/30 hover:bg-white/30 transition-all duration-300 shadow-lg"
|
||||
>
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<h1 className="text-xl sm:text-2xl font-black text-white">Восстановление колена</h1>
|
||||
<p className="text-blue-100 text-sm font-medium">Курс упражнений</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center border border-white/30 shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exercise Slider */}
|
||||
<div className="px-4 sm:px-6 -mt-4 mb-8">
|
||||
<div className="glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl">
|
||||
{/* Exercise Image */}
|
||||
<div className="relative">
|
||||
<img
|
||||
src={currentExercise.image || "/placeholder.svg"}
|
||||
alt={currentExercise.name}
|
||||
className="w-full h-56 sm:h-64 object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-black/10"></div>
|
||||
|
||||
{/* Difficulty Badge */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<div
|
||||
className={`px-4 py-2 rounded-full text-xs font-bold text-white shadow-lg backdrop-blur-sm ${getDifficultyColor(currentExercise.difficulty)}`}
|
||||
>
|
||||
{currentExercise.difficulty}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/20 backdrop-blur-xl rounded-full flex items-center justify-center text-white hover:bg-black/30 transition-all duration-300 shadow-lg border border-white/20"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={nextSlide}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/20 backdrop-blur-xl rounded-full flex items-center justify-center text-white hover:bg-black/30 transition-all duration-300 shadow-lg border border-white/20"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Play Button Overlay */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-20 h-20 bg-white/90 backdrop-blur-xl rounded-full flex items-center justify-center shadow-2xl hover:bg-white transition-all duration-300 transform hover:scale-110 border border-white/50">
|
||||
<svg className="w-8 h-8 text-gray-800 ml-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exercise Info */}
|
||||
<div className="p-6 sm:p-8">
|
||||
<h3 className="text-2xl sm:text-3xl font-black text-gray-800 mb-3">{currentExercise.name}</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed text-sm sm:text-base">{currentExercise.description}</p>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-blue-400 to-cyan-500 rounded-2xl flex items-center justify-center mx-auto mb-2 shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-lg font-black text-gray-800">{currentExercise.duration}</div>
|
||||
<div className="text-xs text-gray-600 font-medium">Время</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-green-400 to-emerald-500 rounded-2xl flex items-center justify-center mx-auto mb-2 shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-lg font-black text-gray-800">{currentExercise.sets}</div>
|
||||
<div className="text-xs text-gray-600 font-medium">Подходы</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-purple-400 to-pink-500 rounded-2xl flex items-center justify-center mx-auto mb-2 shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-lg font-black text-gray-800">{currentExercise.reps}</div>
|
||||
<div className="text-xs text-gray-600 font-medium">Повторы</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-orange-400 to-red-500 rounded-2xl flex items-center justify-center mx-auto mb-2 shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-lg font-black text-gray-800">{currentExercise.calories}</div>
|
||||
<div className="text-xs text-gray-600 font-medium">Калории</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => history.push(`/exercise/${currentExercise.id}`)}
|
||||
className="w-full bg-gradient-to-r from-[#2BACBE] via-blue-500 to-indigo-600 text-white font-black py-4 px-6 rounded-2xl hover:shadow-2xl transition-all duration-300 transform hover:scale-105 shadow-lg backdrop-blur-sm"
|
||||
>
|
||||
🚀 Начать упражнение
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slide indicators */}
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
{exercises.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentSlide(index)}
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
index === currentSlide
|
||||
? "bg-gradient-to-r from-[#2BACBE] to-blue-600 w-8 shadow-lg"
|
||||
: "bg-gray-300 w-2 hover:bg-gray-400"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exercise List */}
|
||||
<div className="px-4 sm:px-6 pb-28">
|
||||
<h2 className="text-xl sm:text-2xl font-black text-gray-800 mb-6">Все упражнения курса</h2>
|
||||
<div className="space-y-4">
|
||||
{exercises.map((exercise, index) => (
|
||||
<div
|
||||
key={exercise.id}
|
||||
onClick={() => history.push(`/exercise/${exercise.id}`)}
|
||||
className={`glass-morphism rounded-2xl p-4 sm:p-6 border border-white/50 shadow-lg cursor-pointer transition-all duration-300 hover:shadow-2xl transform hover:scale-[1.02] backdrop-blur-xl ${
|
||||
index === currentSlide ? "ring-2 ring-[#2BACBE] bg-blue-50/20" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<div className="w-14 h-14 sm:w-16 sm:h-16 bg-gradient-to-br from-[#2BACBE] via-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center text-white font-black text-lg sm:text-xl shadow-xl">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="absolute -top-1 -right-1 w-6 h-6 bg-white rounded-full flex items-center justify-center shadow-lg border border-gray-200">
|
||||
<span className="text-xs">{index < currentSlide ? "✅" : index === currentSlide ? "▶️" : "⏳"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-black text-gray-800 text-lg sm:text-xl truncate">{exercise.name}</h3>
|
||||
<p className="text-gray-600 text-sm mb-2 line-clamp-2">{exercise.description}</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>💪</span>
|
||||
<span>
|
||||
{exercise.sets} × {exercise.reps}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>⏱️</span>
|
||||
<span>{exercise.duration}</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>🔥</span>
|
||||
<span>{exercise.calories} кал</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CourseExercises
|
130
src/pages/Courses.tsx
Normal file
130
src/pages/Courses.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
import { useHistory } from "react-router-dom"
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const currentDate = new Date().toLocaleDateString("ru-RU", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
|
||||
const courses = [
|
||||
{ id: 1, name: "Восстановление колена", progress: 75, color: "from-[#2BACBE] to-[#2BACBE]/80" },
|
||||
{ id: 2, name: "Укрепление спины", progress: 45, color: "from-[#5F5C5C] to-[#5F5C5C]/80" },
|
||||
{ id: 3, name: "Реабилитация плеча", progress: 90, color: "from-[#2BACBE]/80 to-[#5F5C5C]" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 pb-24 overflow-y-auto">
|
||||
{/* Welcome Section */}
|
||||
<div className="pt-12 px-6 mb-8">
|
||||
<div className="bg-white/40 backdrop-blur-2xl rounded-3xl p-6 border border-white/20 shadow-2xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-[#5F5C5C] mb-1">Привет, Александр!</h1>
|
||||
<p className="text-[#5F5C5C]/70 font-medium">{currentDate}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#2BACBE] to-[#2BACBE]/80 backdrop-blur-xl rounded-2xl flex items-center justify-center border border-white/30 shadow-lg">
|
||||
<span className="text-2xl filter drop-shadow-sm">👤</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analytics Cards */}
|
||||
<div className="px-6 mb-8">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-white/30 backdrop-blur-2xl rounded-2xl p-5 border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-black text-[#2BACBE] mb-1">12</div>
|
||||
<div className="text-sm text-[#5F5C5C] font-semibold">Дней подряд</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/30 backdrop-blur-2xl rounded-2xl p-5 border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-black text-[#2BACBE] mb-1">85%</div>
|
||||
<div className="text-sm text-[#5F5C5C] font-semibold">Прогресс</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Exercise */}
|
||||
<div className="px-6 mb-8">
|
||||
<h2 className="text-xl font-black text-[#5F5C5C] mb-4">Текущее упражнение</h2>
|
||||
<div className="bg-white/30 backdrop-blur-2xl rounded-3xl p-6 border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300">
|
||||
<div className="flex items-center space-x-5">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-[#2BACBE] to-[#2BACBE]/80 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<span className="text-3xl filter drop-shadow-sm">🦵</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-black text-[#5F5C5C] text-lg mb-1">Подъемы ног</h3>
|
||||
<p className="text-sm text-[#5F5C5C]/70 font-medium mb-3">Восстановление колена • 3 подхода</p>
|
||||
<div className="bg-white/50 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[#2BACBE] to-[#2BACBE]/80 h-3 rounded-full shadow-sm transition-all duration-500"
|
||||
style={{ width: "60%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => history.push("/exercise/1")}
|
||||
className="bg-gradient-to-r from-[#2BACBE] to-[#2BACBE]/80 text-white px-6 py-3 rounded-2xl font-bold hover:shadow-xl transition-all duration-300 transform hover:scale-105 backdrop-blur-sm"
|
||||
>
|
||||
Продолжить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Courses */}
|
||||
<div className="px-6 mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-black text-[#5F5C5C]">Мои курсы</h2>
|
||||
<button onClick={() => history.push("/courses")} className="text-[#2BACBE] text-sm font-bold hover:underline">
|
||||
Все курсы
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{courses.map((course) => (
|
||||
<div
|
||||
key={course.id}
|
||||
onClick={() => history.push(`/course/${course.id}/exercises`)}
|
||||
className="bg-white/30 backdrop-blur-2xl rounded-3xl p-6 border border-white/20 shadow-xl cursor-pointer hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02]"
|
||||
>
|
||||
<div className="flex items-center space-x-5">
|
||||
<div
|
||||
className={`w-16 h-16 bg-gradient-to-br ${course.color} rounded-2xl flex items-center justify-center shadow-lg`}
|
||||
>
|
||||
<span className="text-2xl text-white filter drop-shadow-sm">💪</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-black text-[#5F5C5C] text-lg mb-2">{course.name}</h3>
|
||||
<div className="bg-white/50 rounded-full h-3 mb-2 overflow-hidden">
|
||||
<div
|
||||
className={`bg-gradient-to-r ${course.color} h-3 rounded-full transition-all duration-700 shadow-sm`}
|
||||
style={{ width: `${course.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-[#5F5C5C]/70 font-semibold">{course.progress}% завершено</p>
|
||||
</div>
|
||||
<div className="text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1">
|
||||
<span className="text-2xl font-bold">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
264
src/pages/Exercise.tsx
Normal file
264
src/pages/Exercise.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useHistory, useParams } from "react-router-dom"
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
|
||||
const Exercise: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [totalTime] = useState(900) // 15 minutes in seconds
|
||||
const [currentSet, setCurrentSet] = useState(1)
|
||||
const [totalSets] = useState(3)
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout
|
||||
if (isPlaying) {
|
||||
interval = setInterval(() => {
|
||||
setCurrentTime((prev) => {
|
||||
if (prev >= totalTime) {
|
||||
setIsPlaying(false)
|
||||
// Show completion animation
|
||||
history.push("/course-complete")
|
||||
return totalTime
|
||||
}
|
||||
return prev + 1
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
return () => clearInterval(interval)
|
||||
}, [isPlaying, totalTime, history])
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const progress = (currentTime / totalTime) * 100
|
||||
|
||||
const exerciseSteps = [
|
||||
{
|
||||
title: "Исходное положение",
|
||||
description:
|
||||
"Лягте на спину на коврик. Руки вдоль тела, ладони прижаты к полу. Ноги выпрямлены, носки направлены вверх. Поясница плотно прижата к полу.",
|
||||
icon: "1️⃣",
|
||||
color: "from-blue-400 to-cyan-500",
|
||||
},
|
||||
{
|
||||
title: "Задание",
|
||||
description:
|
||||
"Медленно поднимите прямые ноги до угла 90 градусов. Задержитесь на 2 секунды, затем медленно опустите ноги, не касаясь пола. Повторите движение плавно и контролируемо.",
|
||||
icon: "2️⃣",
|
||||
color: "from-emerald-400 to-green-500",
|
||||
},
|
||||
{
|
||||
title: "Подходы",
|
||||
description: "Выполните 3 подхода по 12 повторений с отдыхом 60 секунд между подходами.",
|
||||
icon: "3️⃣",
|
||||
color: "from-purple-400 to-pink-500",
|
||||
},
|
||||
{
|
||||
title: "Перерыв",
|
||||
description: "Отдыхайте 60 секунд между подходами. Дышите спокойно и расслабьте мышцы.",
|
||||
icon: "4️⃣",
|
||||
color: "from-orange-400 to-red-500",
|
||||
},
|
||||
{
|
||||
title: "Динамика и статика",
|
||||
description:
|
||||
"Динамическая фаза: подъем и опускание ног выполняется плавно, 2 секунды вверх, 2 секунды вниз. Статическая фаза: удержание ног в верхней точке на 2 секунды.",
|
||||
icon: "5️⃣",
|
||||
color: "from-indigo-400 to-purple-500",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 overflow-y-auto scrollable-content">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-40 bg-gradient-to-br from-[#2BACBE] via-blue-500 to-indigo-600 backdrop-blur-xl">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/10 to-transparent"></div>
|
||||
<div className="relative px-4 sm:px-6 pt-12 pb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<button
|
||||
onClick={() => history.goBack()}
|
||||
className="w-12 h-12 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center border border-white/30 hover:bg-white/30 transition-all duration-300 shadow-lg"
|
||||
>
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<h1 className="text-lg sm:text-xl font-black text-white">Подъемы ног лежа</h1>
|
||||
<p className="text-blue-100 text-sm font-medium">Упражнение 1 из 12</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center border border-white/30 shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set Counter */}
|
||||
<div className="flex justify-center space-x-2">
|
||||
{Array.from({ length: totalSets }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-all duration-300 ${
|
||||
index + 1 <= currentSet
|
||||
? "bg-white text-[#2BACBE] shadow-lg"
|
||||
: "bg-white/20 text-white border border-white/30"
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video/Image Section */}
|
||||
<div className="px-4 sm:px-6 -mt-4 mb-6">
|
||||
<div className="glass-morphism rounded-3xl overflow-hidden shadow-2xl border border-white/50 backdrop-blur-2xl">
|
||||
<div className="relative">
|
||||
<img
|
||||
src="/placeholder.svg?height=250&width=400"
|
||||
alt="Exercise demonstration"
|
||||
className="w-full h-56 sm:h-64 object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-black/10"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<button
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
className="w-20 h-20 bg-white/90 backdrop-blur-xl rounded-full flex items-center justify-center shadow-2xl hover:bg-white transition-all duration-300 transform hover:scale-110 border border-white/50"
|
||||
>
|
||||
<span className="text-3xl text-gray-800 ml-1">{isPlaying ? "⏸️" : "▶️"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Live indicators */}
|
||||
{isPlaying && (
|
||||
<div className="absolute top-4 left-4 flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-white text-sm font-bold bg-black/30 px-3 py-1 rounded-full backdrop-blur-sm">
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timer Display */}
|
||||
<div className="absolute top-4 right-4 bg-black/30 backdrop-blur-sm px-3 py-1 rounded-full">
|
||||
<span className="text-white text-sm font-bold">{formatTime(currentTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exercise Steps */}
|
||||
<div className="px-4 sm:px-6 space-y-4 mb-6">
|
||||
{exerciseSteps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="glass-morphism rounded-2xl p-5 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-xl"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div
|
||||
className={`w-12 h-12 bg-gradient-to-r ${step.color} rounded-xl flex items-center justify-center shadow-lg flex-shrink-0`}
|
||||
>
|
||||
<span className="text-lg">{step.icon}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-gray-800 mb-2">{step.title}</h3>
|
||||
<p className="text-gray-700 leading-relaxed text-sm sm:text-base">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Important Notes */}
|
||||
<div className="px-4 sm:px-6 mb-32">
|
||||
<div className="bg-gradient-to-r from-amber-50 to-orange-50 backdrop-blur-xl rounded-2xl p-5 border border-amber-200/50 shadow-lg">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-r from-amber-400 to-orange-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-sm">!</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-amber-800 mb-2">Важные замечания</h3>
|
||||
<ul className="space-y-1 text-amber-800 text-sm">
|
||||
<li>• Не отрывайте поясницу от пола</li>
|
||||
<li>• Дышите равномерно, не задерживайте дыхание</li>
|
||||
<li>• При болевых ощущениях немедленно прекратите</li>
|
||||
<li>• Движения должны быть плавными и контролируемыми</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Timer at Bottom */}
|
||||
<div className="fixed bottom-20 left-0 right-0 bg-white/95 backdrop-blur-2xl border-t border-gray-200/50 px-4 sm:px-6 py-4 shadow-2xl z-30">
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-sm font-bold text-gray-600">
|
||||
Подход {currentSet} из {totalSets}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-black text-gray-800">
|
||||
{formatTime(currentTime)} / {formatTime(totalTime)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-200 rounded-full h-3 mb-4 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[#2BACBE] via-blue-500 to-indigo-600 h-3 rounded-full transition-all duration-1000 shadow-sm"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
className={`flex-1 font-bold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 ${
|
||||
isPlaying
|
||||
? "bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white shadow-lg"
|
||||
: "bg-gradient-to-r from-[#2BACBE] via-blue-500 to-indigo-600 hover:shadow-xl text-white"
|
||||
}`}
|
||||
>
|
||||
{isPlaying ? "⏸️ Пауза" : "▶️ Начать"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentTime(0)
|
||||
setIsPlaying(false)
|
||||
}}
|
||||
className="px-6 py-3 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold rounded-xl transition-all duration-300 hover:shadow-lg"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentSet((prev) => Math.min(prev + 1, totalSets))}
|
||||
className="px-6 py-3 bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-600 hover:to-green-700 text-white font-bold rounded-xl transition-all duration-300 hover:shadow-lg"
|
||||
>
|
||||
✅
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Exercise
|
@ -1,239 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonProgressBar,
|
||||
} from "@ionic/react"
|
||||
import { playOutline, pauseOutline, stopOutline, timerOutline, fitnessOutline, trophyOutline } from "ionicons/icons"
|
||||
import ExerciseItem from "../components/ExerciseItem" // Импортируем новый компонент
|
||||
|
||||
interface Exercise {
|
||||
id: number
|
||||
name: string
|
||||
duration: number // в секундах
|
||||
description: string
|
||||
difficulty: "easy" | "medium" | "hard"
|
||||
completed: boolean
|
||||
videoUrl?: string // Добавлено поле для URL видео
|
||||
}
|
||||
|
||||
const ExercisesPage: React.FC = () => {
|
||||
const [currentExercise, setCurrentExercise] = useState<Exercise | null>(null)
|
||||
const [timeLeft, setTimeLeft] = useState(0)
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
const [isAnyVideoPlaying, setIsAnyVideoPlaying] = useState(false) // Новое состояние для отслеживания видео
|
||||
|
||||
const initialExercises: Exercise[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Разминка плечевого сустава",
|
||||
duration: 300, // 5 минут
|
||||
description: "Медленные круговые движения плечами для восстановления подвижности",
|
||||
difficulty: "easy",
|
||||
completed: false, // Изначально все не выполнено
|
||||
videoUrl: "/placeholder.svg?height=200&width=300", // Пример видео URL
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Упражнения для кисти",
|
||||
duration: 600, // 10 минут
|
||||
description: "Сжимание и разжимание кулака, вращения кистью",
|
||||
difficulty: "medium",
|
||||
completed: false,
|
||||
videoUrl: "/placeholder.svg?height=200&width=300", // Пример видео URL
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Растяжка мышц руки",
|
||||
duration: 450, // 7.5 минут
|
||||
description: "Статические упражнения на растяжку поврежденных мышц",
|
||||
difficulty: "hard",
|
||||
completed: false,
|
||||
videoUrl: "/placeholder.svg?height=200&width=300", // Пример видео URL
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Укрепление предплечья",
|
||||
duration: 400, // 6.6 минут
|
||||
description: "Упражнения с легкими гантелями для укрепления мышц предплечья",
|
||||
difficulty: "medium",
|
||||
completed: false,
|
||||
videoUrl: "/placeholder.svg?height=200&width=300", // Пример видео URL
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Координационные упражнения",
|
||||
duration: 500, // 8.3 минут
|
||||
description: "Упражнения на баланс и мелкую моторику",
|
||||
difficulty: "hard",
|
||||
completed: false,
|
||||
videoUrl: "/placeholder.svg?height=200&width=300", // Пример видео URL
|
||||
},
|
||||
]
|
||||
|
||||
const [exerciseList, setExerciseList] = useState<Exercise[]>(() => {
|
||||
// Попытка загрузить состояние из localStorage
|
||||
if (typeof window !== "undefined") {
|
||||
const savedExercises = localStorage.getItem("rehab_exercises")
|
||||
if (savedExercises) {
|
||||
return JSON.parse(savedExercises)
|
||||
}
|
||||
}
|
||||
return initialExercises
|
||||
})
|
||||
|
||||
// Сохранение состояния в localStorage при изменении
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("rehab_exercises", JSON.stringify(exerciseList))
|
||||
}
|
||||
}, [exerciseList])
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | undefined
|
||||
|
||||
if (isRunning && !isPaused && timeLeft > 0) {
|
||||
interval = setInterval(() => {
|
||||
setTimeLeft((prevTime) => prevTime - 1)
|
||||
}, 1000)
|
||||
} else if (timeLeft === 0 && currentExercise && isRunning) {
|
||||
// Упражнение завершено
|
||||
setIsRunning(false)
|
||||
setCurrentExercise(null)
|
||||
// Отмечаем упражнение как выполненное
|
||||
setExerciseList((prev) => prev.map((ex) => (ex.id === currentExercise.id ? { ...ex, completed: true } : ex)))
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [isRunning, isPaused, timeLeft, currentExercise])
|
||||
|
||||
const startExercise = (exercise: Exercise) => {
|
||||
setCurrentExercise(exercise)
|
||||
setTimeLeft(exercise.duration)
|
||||
setIsRunning(true)
|
||||
setIsPaused(false)
|
||||
}
|
||||
|
||||
const pauseExercise = () => {
|
||||
setIsPaused((prev) => !prev)
|
||||
}
|
||||
|
||||
const stopExercise = () => {
|
||||
setIsRunning(false)
|
||||
setIsPaused(false)
|
||||
setCurrentExercise(null)
|
||||
setTimeLeft(0)
|
||||
}
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const completedCount = exerciseList.filter((ex) => ex.completed).length
|
||||
const totalCount = exerciseList.length
|
||||
const progressPercentage = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
||||
|
||||
// Обработчик для отслеживания состояния воспроизведения видео
|
||||
const handleVideoPlayToggle = (isPlaying: boolean) => {
|
||||
setIsAnyVideoPlaying(isPlaying)
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent className="bg-gradient-to-br from-slate-100 via-teal-50 to-cyan-50">
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Общий прогресс */}
|
||||
<IonCard className="bg-white/80 backdrop-blur-sm border border-teal-200/50 shadow-lg">
|
||||
<IonCardHeader>
|
||||
<IonCardTitle className="text-slate-800 flex items-center gap-2">
|
||||
<IonIcon icon={trophyOutline} className="text-teal-600" />
|
||||
Общий прогресс
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Выполнено упражнений</span>
|
||||
<span className="font-bold text-teal-600">
|
||||
{completedCount}/{totalCount}
|
||||
</span>
|
||||
</div>
|
||||
<IonProgressBar value={progressPercentage / 100} className="h-2 rounded-full" color="success" />
|
||||
<div className="text-center text-sm text-slate-500">{progressPercentage.toFixed(0)}% завершено</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
|
||||
{/* Текущее упражнение (если активно) */}
|
||||
{currentExercise && (
|
||||
<IonCard className="bg-gradient-to-r from-teal-500 to-cyan-500 text-white shadow-xl">
|
||||
<IonCardHeader>
|
||||
<IonCardTitle className="flex items-center gap-2">
|
||||
<IonIcon icon={timerOutline} />
|
||||
Текущее упражнение
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{currentExercise.name}</h3>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold mb-2">{formatTime(timeLeft)}</div>
|
||||
<IonProgressBar
|
||||
value={(currentExercise.duration - timeLeft) / currentExercise.duration}
|
||||
className="h-2 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<IonButton fill="outline" color="light" onClick={pauseExercise}>
|
||||
<IonIcon icon={isPaused ? playOutline : pauseOutline} slot="start" />
|
||||
{isPaused ? "Продолжить" : "Пауза"}
|
||||
</IonButton>
|
||||
<IonButton fill="outline" color="light" onClick={stopExercise}>
|
||||
<IonIcon icon={stopOutline} slot="start" />
|
||||
Остановить
|
||||
</IonButton>
|
||||
</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
)}
|
||||
|
||||
{/* Список упражнений */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
|
||||
<IonIcon icon={fitnessOutline} className="text-teal-600" />
|
||||
Список упражнений
|
||||
</h2>
|
||||
|
||||
{exerciseList.map((exercise) => (
|
||||
<ExerciseItem
|
||||
key={exercise.id}
|
||||
exercise={exercise}
|
||||
onStart={startExercise}
|
||||
isCurrent={currentExercise?.id === exercise.id}
|
||||
isDisabled={!!currentExercise || isAnyVideoPlaying} // Отключаем, если есть активное упражнение или видео
|
||||
onVideoPlayToggle={handleVideoPlayToggle} // Передаем обработчик
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExercisesPage
|
125
src/pages/ForgotPassword.tsx
Normal file
125
src/pages/ForgotPassword.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
|
||||
const ForgotPassword: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const [email, setEmail] = useState("")
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitted(true)
|
||||
// Simulate sending reset email
|
||||
setTimeout(() => {
|
||||
history.push("/login")
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-[#3ABBC7] to-[#0D212C] flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<button
|
||||
onClick={() => history.goBack()}
|
||||
className="absolute top-6 left-6 w-12 h-12 bg-white/10 backdrop-blur-lg rounded-xl flex items-center justify-center border border-white/20 text-white hover:bg-white/20 transition-all"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="w-20 h-20 bg-white/10 backdrop-blur-2xl rounded-2xl flex items-center justify-center mx-auto mb-4 border border-white/20 shadow-2xl">
|
||||
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-black text-white mb-2">Забыли пароль?</h1>
|
||||
<p className="text-white/80 font-medium">
|
||||
{isSubmitted ? "Проверьте вашу почту" : "Введите email для восстановления пароля"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="glass-morphism rounded-3xl p-8 border border-white/20 shadow-2xl">
|
||||
{!isSubmitted ? (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Email Input */}
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Электронная почта</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="example@mail.com"
|
||||
required
|
||||
/>
|
||||
<svg
|
||||
className="absolute right-3 top-3.5 w-5 h-5 text-white/60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-white/20 backdrop-blur-lg hover:bg-white/30 text-white font-bold py-3 px-6 rounded-2xl border border-white/30 transition-all duration-300 transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
Отправить ссылку
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="text-center space-y-6">
|
||||
{/* Success Icon */}
|
||||
<div className="w-16 h-16 bg-green-500/20 backdrop-blur-lg rounded-full flex items-center justify-center mx-auto border border-green-400/30">
|
||||
<svg className="w-8 h-8 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Письмо отправлено!</h3>
|
||||
<p className="text-white/80 text-sm leading-relaxed">
|
||||
Мы отправили инструкции по восстановлению пароля на адрес{" "}
|
||||
<span className="font-semibold">{email}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-white/60 text-xs mb-4">Автоматический переход через 3 секунды...</p>
|
||||
<button
|
||||
onClick={() => history.push("/login")}
|
||||
className="text-white/80 text-sm font-medium hover:text-white transition-colors"
|
||||
>
|
||||
Вернуться к входу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPassword
|
@ -1,268 +1,235 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonProgressBar,
|
||||
IonBadge,
|
||||
} from "@ionic/react"
|
||||
import { playOutline, pauseOutline, stopOutline, timerOutline, fitnessOutline, trophyOutline } from "ionicons/icons"
|
||||
|
||||
import Header from "../components/Header"
|
||||
import Footer from "../components/Footer"
|
||||
|
||||
interface Exercise {
|
||||
id: number
|
||||
name: string
|
||||
duration: number // в секундах
|
||||
description: string
|
||||
difficulty: "easy" | "medium" | "hard"
|
||||
completed: boolean
|
||||
}
|
||||
import { useEffect, useState } from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const [currentExercise, setCurrentExercise] = useState<Exercise | null>(null)
|
||||
const [timeLeft, setTimeLeft] = useState(0)
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
const history = useHistory()
|
||||
const [currentDate, setCurrentDate] = useState("")
|
||||
|
||||
const exercises: Exercise[] = [
|
||||
useEffect(() => {
|
||||
setCurrentDate(
|
||||
new Date().toLocaleDateString("ru-RU", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
)
|
||||
}, [])
|
||||
|
||||
const stats = [
|
||||
{ value: "12", label: "Дней подряд", icon: "🔥", color: "from-orange-400 to-red-500" },
|
||||
{ value: "85%", label: "Прогресс", icon: "📈", color: "from-emerald-400 to-green-500" },
|
||||
{ value: "3", label: "Курса", icon: "📚", color: "from-blue-400 to-cyan-500" },
|
||||
{ value: "45", label: "Минут", icon: "⏱️", color: "from-purple-400 to-pink-500" },
|
||||
]
|
||||
|
||||
const courses = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Разминка плечевого сустава",
|
||||
duration: 300, // 5 минут
|
||||
description: "Медленные круговые движения плечами для восстановления подвижности",
|
||||
difficulty: "easy",
|
||||
completed: true,
|
||||
name: "Восстановление колена",
|
||||
progress: 75,
|
||||
color: "from-[#2BACBE] to-blue-600",
|
||||
icon: "🦵",
|
||||
exercises: 12,
|
||||
nextExercise: "Подъемы ног лежа",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Упражнения для кисти",
|
||||
duration: 600, // 10 минут
|
||||
description: "Сжимание и разжимание кулака, вращения кистью",
|
||||
difficulty: "medium",
|
||||
completed: false,
|
||||
name: "Укрепление спины",
|
||||
progress: 45,
|
||||
color: "from-emerald-500 to-green-600",
|
||||
icon: "🏃♂️",
|
||||
exercises: 8,
|
||||
nextExercise: "Планка",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Растяжка мышц руки",
|
||||
duration: 450, // 7.5 минут
|
||||
description: "Статические упражнения на растяжку поврежденных мышц",
|
||||
difficulty: "hard",
|
||||
completed: false,
|
||||
name: "Реабилитация плеча",
|
||||
progress: 90,
|
||||
color: "from-purple-500 to-pink-600",
|
||||
icon: "💪",
|
||||
exercises: 10,
|
||||
nextExercise: "Вращения плечами",
|
||||
},
|
||||
]
|
||||
|
||||
const [exerciseList, setExerciseList] = useState(exercises)
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout
|
||||
|
||||
if (isRunning && !isPaused && timeLeft > 0) {
|
||||
interval = setInterval(() => {
|
||||
setTimeLeft(timeLeft - 1)
|
||||
}, 1000)
|
||||
} else if (timeLeft === 0 && currentExercise) {
|
||||
// Упражнение завершено
|
||||
setIsRunning(false)
|
||||
setCurrentExercise(null)
|
||||
// Отмечаем упражнение как выполненное
|
||||
setExerciseList((prev) => prev.map((ex) => (ex.id === currentExercise.id ? { ...ex, completed: true } : ex)))
|
||||
}
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [isRunning, isPaused, timeLeft, currentExercise])
|
||||
|
||||
const startExercise = (exercise: Exercise) => {
|
||||
setCurrentExercise(exercise)
|
||||
setTimeLeft(exercise.duration)
|
||||
setIsRunning(true)
|
||||
setIsPaused(false)
|
||||
}
|
||||
|
||||
const pauseExercise = () => {
|
||||
setIsPaused(!isPaused)
|
||||
}
|
||||
|
||||
const stopExercise = () => {
|
||||
setIsRunning(false)
|
||||
setIsPaused(false)
|
||||
setCurrentExercise(null)
|
||||
setTimeLeft(0)
|
||||
}
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case "easy":
|
||||
return "text-emerald-400"
|
||||
case "medium":
|
||||
return "text-yellow-400"
|
||||
case "hard":
|
||||
return "text-red-400"
|
||||
default:
|
||||
return "text-slate-400"
|
||||
}
|
||||
}
|
||||
|
||||
const getDifficultyBg = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case "easy":
|
||||
return "bg-emerald-500/20 border-emerald-500/30"
|
||||
case "medium":
|
||||
return "bg-yellow-500/20 border-yellow-500/30"
|
||||
case "hard":
|
||||
return "bg-red-500/20 border-red-500/30"
|
||||
default:
|
||||
return "bg-slate-500/20 border-slate-500/30"
|
||||
}
|
||||
}
|
||||
|
||||
const completedCount = exerciseList.filter((ex) => ex.completed).length
|
||||
const totalCount = exerciseList.length
|
||||
const progressPercentage = (completedCount / totalCount) * 100
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent className="bg-gradient-to-br from-slate-100 via-teal-50 to-cyan-50">
|
||||
<Header />
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Приветствие и статистика */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-slate-800 mb-2">Добро пожаловать, Иван!</h1>
|
||||
<p className="text-slate-600">Продолжайте восстановление. Вы на правильном пути!</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 overflow-y-auto scrollable-content">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-[#3ABBC7] to-[#0D212C] pt-12 pb-8 px-4 sm:px-6 relative overflow-hidden">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-10 left-10 w-20 h-20 bg-white rounded-full animate-pulse"></div>
|
||||
<div className="absolute bottom-10 right-10 w-16 h-16 bg-white rounded-full animate-bounce"></div>
|
||||
<div className="absolute top-1/2 right-1/4 w-12 h-12 bg-white rounded-full animate-ping"></div>
|
||||
<div className="absolute top-1/4 left-1/3 w-8 h-8 bg-white rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-black text-white mb-2">Привет, Александр! 👋</h1>
|
||||
<p className="text-white/80 font-medium text-sm sm:text-base">{currentDate}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center border border-white/30 shadow-xl relative">
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute -top-1 -right-1 w-5 h-5 bg-green-400 rounded-full border-2 border-white animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Общий прогресс */}
|
||||
<IonCard className="bg-white/80 backdrop-blur-sm border border-teal-200/50 shadow-lg">
|
||||
<IonCardHeader>
|
||||
<IonCardTitle className="text-slate-800 flex items-center gap-2">
|
||||
<IonIcon icon={trophyOutline} className="text-teal-600" />
|
||||
Общий прогресс
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Выполнено упражнений</span>
|
||||
<span className="font-bold text-teal-600">
|
||||
{completedCount}/{totalCount}
|
||||
</span>
|
||||
</div>
|
||||
<IonProgressBar value={progressPercentage / 100} className="h-2 rounded-full" color="success" />
|
||||
<div className="text-center text-sm text-slate-500">{progressPercentage.toFixed(0)}% завершено</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
|
||||
{/* Текущее упражнение (если активно) */}
|
||||
{currentExercise && (
|
||||
<IonCard className="bg-gradient-to-r from-teal-500 to-cyan-500 text-white shadow-xl">
|
||||
<IonCardHeader>
|
||||
<IonCardTitle className="flex items-center gap-2">
|
||||
<IonIcon icon={timerOutline} />
|
||||
Текущее упражнение
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{currentExercise.name}</h3>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold mb-2">{formatTime(timeLeft)}</div>
|
||||
<IonProgressBar
|
||||
value={(currentExercise.duration - timeLeft) / currentExercise.duration}
|
||||
className="h-2 rounded-full mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<IonButton fill="outline" color="light" onClick={pauseExercise}>
|
||||
<IonIcon icon={isPaused ? playOutline : pauseOutline} slot="start" />
|
||||
{isPaused ? "Продолжить" : "Пауза"}
|
||||
</IonButton>
|
||||
<IonButton fill="outline" color="light" onClick={stopExercise}>
|
||||
<IonIcon icon={stopOutline} slot="start" />
|
||||
Остановить
|
||||
</IonButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
)}
|
||||
|
||||
{/* Список упражнений */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
|
||||
<IonIcon icon={fitnessOutline} className="text-teal-600" />
|
||||
Сегодняшние упражнения
|
||||
</h2>
|
||||
|
||||
{exerciseList.map((exercise) => (
|
||||
<IonCard
|
||||
key={exercise.id}
|
||||
className={`${exercise.completed ? "bg-emerald-50/80" : "bg-white/80"} backdrop-blur-sm border border-teal-200/50 shadow-md`}
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/15 backdrop-blur-xl rounded-2xl p-4 border border-white/20 shadow-lg hover:bg-white/20 transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
<IonCardContent>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${exercise.completed ? "text-emerald-700" : "text-slate-800"}`}>
|
||||
{exercise.name}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600 mt-1">{exercise.description}</p>
|
||||
</div>
|
||||
{exercise.completed && (
|
||||
<IonBadge color="success" className="ml-2">
|
||||
Выполнено
|
||||
</IonBadge>
|
||||
)}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-r ${stat.color} rounded-xl flex items-center justify-center shadow-lg`}
|
||||
>
|
||||
<span className="text-lg sm:text-xl">{stat.icon}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-slate-500">{formatTime(exercise.duration)}</span>
|
||||
<IonBadge
|
||||
className={`${getDifficultyBg(exercise.difficulty)} ${getDifficultyColor(exercise.difficulty)} border`}
|
||||
>
|
||||
{exercise.difficulty === "easy"
|
||||
? "Легко"
|
||||
: exercise.difficulty === "medium"
|
||||
? "Средне"
|
||||
: "Сложно"}
|
||||
</IonBadge>
|
||||
</div>
|
||||
|
||||
{!exercise.completed && !currentExercise && (
|
||||
<IonButton
|
||||
size="small"
|
||||
onClick={() => startExercise(exercise)}
|
||||
className="bg-gradient-to-r from-teal-500 to-cyan-500"
|
||||
>
|
||||
<IonIcon icon={playOutline} slot="start" />
|
||||
Начать
|
||||
</IonButton>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xl sm:text-2xl font-black text-white">{stat.value}</div>
|
||||
<div className="text-white/70 text-xs sm:text-sm font-medium">{stat.label}</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
<Footer />
|
||||
</IonPage>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 space-y-8 -mt-4 pb-28">
|
||||
{/* Current Exercise */}
|
||||
<div className="glass-morphism rounded-3xl p-6 border border-white/50 shadow-2xl backdrop-blur-2xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl sm:text-2xl font-black text-gray-800">Текущее упражнение</h2>
|
||||
<div className="px-3 py-1 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full shadow-lg">
|
||||
<span className="text-white text-sm font-bold">Активно</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-[#2BACBE] via-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center shadow-xl">
|
||||
<span className="text-3xl">🦵</span>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-gradient-to-r from-orange-400 to-red-500 rounded-full flex items-center justify-center shadow-lg animate-pulse">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-black text-gray-800 text-lg mb-1">Подъемы ног лежа</h3>
|
||||
<p className="text-gray-600 font-medium mb-3 text-sm">Восстановление колена • 3 подхода по 12</p>
|
||||
|
||||
<div className="relative">
|
||||
<div className="bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[#2BACBE] via-blue-500 to-indigo-600 h-3 rounded-full shadow-sm transition-all duration-700"
|
||||
style={{ width: "60%" }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="absolute right-0 -top-6 text-sm font-bold text-gray-600">60%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => history.push("/exercise/1")}
|
||||
className="bg-gradient-to-r from-[#2BACBE] via-blue-500 to-indigo-600 text-white px-4 sm:px-6 py-3 rounded-2xl font-bold hover:shadow-xl transition-all duration-300 transform hover:scale-105 backdrop-blur-sm text-sm sm:text-base"
|
||||
>
|
||||
Продолжить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="glass-morphism rounded-2xl p-4 text-center border border-white/50 shadow-lg backdrop-blur-xl">
|
||||
<div className="text-2xl font-black text-[#2BACBE] mb-1">24</div>
|
||||
<div className="text-xs text-gray-600 font-semibold">Упражнений</div>
|
||||
</div>
|
||||
<div className="glass-morphism rounded-2xl p-4 text-center border border-white/50 shadow-lg backdrop-blur-xl">
|
||||
<div className="text-2xl font-black text-emerald-500 mb-1">180</div>
|
||||
<div className="text-xs text-gray-600 font-semibold">Минут</div>
|
||||
</div>
|
||||
<div className="glass-morphism rounded-2xl p-4 text-center border border-white/50 shadow-lg backdrop-blur-xl">
|
||||
<div className="text-2xl font-black text-purple-500 mb-1">7</div>
|
||||
<div className="text-xs text-gray-600 font-semibold">Дней</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Courses */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl sm:text-2xl font-black text-gray-800">Мои курсы</h2>
|
||||
<button
|
||||
onClick={() => history.push("/courses")}
|
||||
className="text-[#2BACBE] text-sm font-bold hover:underline flex items-center space-x-1"
|
||||
>
|
||||
<span>Все курсы</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{courses.map((course) => (
|
||||
<div
|
||||
key={course.id}
|
||||
onClick={() => history.push(`/course/${course.id}/exercises`)}
|
||||
className="glass-morphism rounded-3xl p-6 border border-white/50 shadow-xl cursor-pointer hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02] backdrop-blur-2xl"
|
||||
>
|
||||
<div className="flex items-center space-x-5">
|
||||
<div
|
||||
className={`w-16 h-16 bg-gradient-to-br ${course.color} rounded-2xl flex items-center justify-center shadow-xl relative`}
|
||||
>
|
||||
<span className="text-2xl text-white filter drop-shadow-sm">{course.icon}</span>
|
||||
<div className="absolute -top-1 -right-1 w-5 h-5 bg-white rounded-full flex items-center justify-center shadow-lg">
|
||||
<span className="text-xs font-bold text-gray-600">{course.exercises}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-black text-gray-800 text-lg mb-1">{course.name}</h3>
|
||||
<p className="text-gray-600 text-sm mb-2">Следующее: {course.nextExercise}</p>
|
||||
<div className="bg-gray-200 rounded-full h-3 mb-2 overflow-hidden">
|
||||
<div
|
||||
className={`bg-gradient-to-r ${course.color} h-3 rounded-full transition-all duration-700 shadow-sm`}
|
||||
style={{ width: `${course.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 font-semibold">{course.progress}% завершено</p>
|
||||
</div>
|
||||
<div className="text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,103 +2,149 @@
|
||||
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonText,
|
||||
} from "@ionic/react"
|
||||
import { mailOutline, lockClosedOutline, logInOutline } from "ionicons/icons"
|
||||
import { useHistory } from "react-router-dom"
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const handleLogin = () => {
|
||||
// Логика входа
|
||||
console.log("Login:", { email, password })
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Simulate login
|
||||
history.push("/home")
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent className="bg-gradient-to-br from-slate-100 via-teal-50 to-cyan-50">
|
||||
<div className="flex items-center justify-center min-h-full p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Логотип */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-teal-600 to-cyan-600 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-teal-500/20">
|
||||
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
<div className="min-h-screen bg-gradient-to-br from-[#3ABBC7] to-[#0D212C] flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-20 h-20 bg-white/10 backdrop-blur-2xl rounded-2xl flex items-center justify-center mx-auto mb-4 border border-white/20 shadow-2xl">
|
||||
<svg width="40" height="40" viewBox="0 0 100 100" fill="none" className="text-white">
|
||||
<circle cx="50" cy="20" r="8" fill="currentColor" />
|
||||
<path
|
||||
d="M50 28 L50 60 M50 45 L35 35 M50 45 L65 35 M50 60 L35 80 M50 60 L65 80"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-black text-white mb-2">Добро пожаловать!</h1>
|
||||
<p className="text-white/80 font-medium">Войдите в свой аккаунт</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="glass-morphism rounded-3xl p-8 border border-white/20 shadow-2xl">
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
{/* Email Input */}
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Электронная почта</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="example@mail.com"
|
||||
required
|
||||
/>
|
||||
<svg
|
||||
className="absolute right-3 top-3.5 w-5 h-5 text-white/60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-teal-600 to-cyan-600 bg-clip-text text-transparent mb-2">
|
||||
РеабилитацияПро
|
||||
</h1>
|
||||
<p className="text-slate-600">Восстановление после травм</p>
|
||||
</div>
|
||||
|
||||
{/* Форма входа */}
|
||||
<IonCard className="bg-white/80 backdrop-blur-sm border border-teal-200/50 shadow-xl">
|
||||
<IonCardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={mailOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Email
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
type="email"
|
||||
value={email}
|
||||
onIonInput={(e) => setEmail(e.detail.value!)}
|
||||
placeholder="your@email.com"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
{/* Password Input */}
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Пароль</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-3.5 text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={lockClosedOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Пароль
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
type="password"
|
||||
value={password}
|
||||
onIonInput={(e) => setPassword(e.detail.value!)}
|
||||
placeholder="••••••••"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
{/* Forgot Password */}
|
||||
<div className="text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => history.push("/forgot-password")}
|
||||
className="text-white/80 text-sm font-medium hover:text-white transition-colors"
|
||||
>
|
||||
Забыли пароль?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={handleLogin}
|
||||
className="bg-gradient-to-r from-teal-600 to-cyan-600 mt-6"
|
||||
>
|
||||
<IonIcon icon={logInOutline} slot="start" />
|
||||
Войти
|
||||
</IonButton>
|
||||
{/* Login Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-white/20 backdrop-blur-lg hover:bg-white/30 text-white font-bold py-3 px-6 rounded-2xl border border-white/30 transition-all duration-300 transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
Войти
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<IonText className="text-slate-600">
|
||||
Нет аккаунта?{" "}
|
||||
<a href="/register" className="text-teal-600 font-medium hover:text-teal-700">
|
||||
Зарегистрироваться
|
||||
</a>
|
||||
</IonText>
|
||||
</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
{/* Register Link */}
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-white/80 text-sm">
|
||||
Нет аккаунта?{" "}
|
||||
<button onClick={() => history.push("/register")} className="text-white font-semibold hover:underline">
|
||||
Зарегистрироваться
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,167 +2,226 @@
|
||||
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonText,
|
||||
IonSelect,
|
||||
IonSelectOption,
|
||||
} from "@ionic/react"
|
||||
import { personOutline, mailOutline, lockClosedOutline, medkitOutline, personAddOutline } from "ionicons/icons"
|
||||
import { useHistory } from "react-router-dom"
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
injuryType: "",
|
||||
})
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
|
||||
const handleRegister = () => {
|
||||
// Логика регистрации
|
||||
console.log("Register:", formData)
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
alert("Пароли не совпадают")
|
||||
return
|
||||
}
|
||||
// Simulate registration
|
||||
history.push("/home")
|
||||
}
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent className="bg-gradient-to-br from-slate-100 via-teal-50 to-cyan-50">
|
||||
<div className="flex items-center justify-center min-h-full p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Заголовок */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-slate-800 mb-2">Регистрация пациента</h1>
|
||||
<p className="text-slate-600">Создайте аккаунт для начала реабилитации</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-[#3ABBC7] to-[#0D212C] flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<button
|
||||
onClick={() => history.goBack()}
|
||||
className="absolute top-6 left-6 w-12 h-12 bg-white/10 backdrop-blur-lg rounded-xl flex items-center justify-center border border-white/20 text-white hover:bg-white/20 transition-all"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="w-20 h-20 bg-white/10 backdrop-blur-2xl rounded-2xl flex items-center justify-center mx-auto mb-4 border border-white/20 shadow-2xl">
|
||||
<svg width="40" height="40" viewBox="0 0 100 100" fill="none" className="text-white">
|
||||
<circle cx="50" cy="20" r="8" fill="currentColor" />
|
||||
<path
|
||||
d="M50 28 L50 60 M50 45 L35 35 M50 45 L65 35 M50 60 L35 80 M50 60 L65 80"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-black text-white mb-2">Регистрация</h1>
|
||||
<p className="text-white/80 font-medium">Создайте новый аккаунт</p>
|
||||
</div>
|
||||
|
||||
{/* Registration Form */}
|
||||
<div className="glass-morphism rounded-3xl p-8 border border-white/20 shadow-2xl">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Name Fields */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Имя</label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleInputChange}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="Иван"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Фамилия</label>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleInputChange}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="Иванов"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Форма регистрации */}
|
||||
<IonCard className="bg-white/80 backdrop-blur-sm border border-teal-200/50 shadow-xl">
|
||||
<IonCardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={personOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Имя
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
value={formData.firstName}
|
||||
onIonInput={(e) => updateField("firstName", e.detail.value!)}
|
||||
placeholder="Иван"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Электронная почта</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="example@mail.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={personOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Фамилия
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
value={formData.lastName}
|
||||
onIonInput={(e) => updateField("lastName", e.detail.value!)}
|
||||
placeholder="Петров"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Пароль</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-3.5 text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={mailOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Email
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onIonInput={(e) => updateField("email", e.detail.value!)}
|
||||
placeholder="your@email.com"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label className="block text-white/90 text-sm font-semibold mb-2">Подтвердите пароль</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
className="w-full bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl px-4 py-3 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent transition-all"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-3 top-3.5 text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={medkitOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Тип травмы
|
||||
</IonLabel>
|
||||
<IonSelect
|
||||
value={formData.injuryType}
|
||||
onIonChange={(e) => updateField("injuryType", e.detail.value)}
|
||||
placeholder="Выберите тип травмы"
|
||||
className="text-slate-800"
|
||||
>
|
||||
<IonSelectOption value="arm">Травма руки</IonSelectOption>
|
||||
<IonSelectOption value="leg">Травма ноги</IonSelectOption>
|
||||
<IonSelectOption value="back">Травма спины</IonSelectOption>
|
||||
<IonSelectOption value="shoulder">Травма плеча</IonSelectOption>
|
||||
<IonSelectOption value="other">Другое</IonSelectOption>
|
||||
</IonSelect>
|
||||
</IonItem>
|
||||
{/* Register Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-white/20 backdrop-blur-lg hover:bg-white/30 text-white font-bold py-3 px-6 rounded-2xl border border-white/30 transition-all duration-300 transform hover:scale-105 shadow-lg mt-6"
|
||||
>
|
||||
Зарегистрироваться
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={lockClosedOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Пароль
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onIonInput={(e) => updateField("password", e.detail.value!)}
|
||||
placeholder="••••••••"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
|
||||
<IonItem className="bg-slate-50/50 rounded-lg border border-slate-200/50">
|
||||
<IonIcon icon={lockClosedOutline} slot="start" className="text-teal-600" />
|
||||
<IonLabel position="stacked" className="text-slate-700">
|
||||
Подтвердите пароль
|
||||
</IonLabel>
|
||||
<IonInput
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onIonInput={(e) => updateField("confirmPassword", e.detail.value!)}
|
||||
placeholder="••••••••"
|
||||
className="text-slate-800"
|
||||
/>
|
||||
</IonItem>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={handleRegister}
|
||||
className="bg-gradient-to-r from-teal-600 to-cyan-600 mt-6"
|
||||
>
|
||||
<IonIcon icon={personAddOutline} slot="start" />
|
||||
Зарегистрироваться
|
||||
</IonButton>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<IonText className="text-slate-600">
|
||||
Уже есть аккаунт?{" "}
|
||||
<a href="/login" className="text-teal-600 font-medium hover:text-teal-700">
|
||||
Войти
|
||||
</a>
|
||||
</IonText>
|
||||
</div>
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
{/* Login Link */}
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-white/80 text-sm">
|
||||
Уже есть аккаунт?{" "}
|
||||
<button onClick={() => history.push("/login")} className="text-white font-semibold hover:underline">
|
||||
Войти
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
204
src/pages/Settings.tsx
Normal file
204
src/pages/Settings.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
import BottomNavigation from "../components/BottomNavigation"
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const handleLogout = () => {
|
||||
// Simulate logout
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pb-20">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-gray-600 to-gray-800 px-4 pt-12 pb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Настройки</h1>
|
||||
<p className="text-gray-300">Профиль пациента</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-white/20 backdrop-blur-lg rounded-full flex items-center justify-center border border-white/30">
|
||||
<span className="text-2xl">👤</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Information */}
|
||||
<div className="px-4 -mt-4 mb-6">
|
||||
<div className="bg-white/80 backdrop-blur-lg rounded-2xl p-6 border border-gray-200/50 shadow-lg">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Личная информация</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Имя</label>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<span className="text-gray-800">Александр Петров</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Email</label>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<span className="text-gray-800">alexander.petrov@email.com</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Логин</label>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<span className="text-gray-800">alex_petrov</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Дата регистрации</label>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<span className="text-gray-800">15 марта 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Medical Information */}
|
||||
<div className="px-4 mb-6">
|
||||
<div className="bg-white/80 backdrop-blur-lg rounded-2xl p-6 border border-gray-200/50 shadow-lg">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Медицинская информация</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Тип травмы</label>
|
||||
<div className="bg-red-50 rounded-lg p-3 border border-red-200">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-red-600">🦵</span>
|
||||
<span className="text-red-800 font-medium">Травма колена</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Описание травмы</label>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<p className="text-gray-800 text-sm leading-relaxed">
|
||||
Повреждение передней крестообразной связки левого колена. Получена во время игры в футбол 12 февраля
|
||||
2024 года. Проведена артроскопическая операция 20 февраля 2024 года.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Лечащий врач</label>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<span className="text-gray-800">Доктор Иванов И.И.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">Стадия восстановления</label>
|
||||
<div className="bg-green-50 rounded-lg p-3 border border-green-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-green-800 font-medium">Активная реабилитация</span>
|
||||
<span className="text-green-600 text-sm">75% завершено</span>
|
||||
</div>
|
||||
<div className="mt-2 bg-green-200 rounded-full h-2">
|
||||
<div className="bg-green-500 h-2 rounded-full" style={{ width: "75%" }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="px-4 mb-6">
|
||||
<div className="bg-white/80 backdrop-blur-lg rounded-2xl p-6 border border-gray-200/50 shadow-lg">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Статистика</h2>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">28</div>
|
||||
<div className="text-sm text-gray-600">Дней занятий</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">156</div>
|
||||
<div className="text-sm text-gray-600">Упражнений выполнено</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">3</div>
|
||||
<div className="text-sm text-gray-600">Курса завершено</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-600">42</div>
|
||||
<div className="text-sm text-gray-600">Часов тренировок</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Options */}
|
||||
<div className="px-4 mb-6">
|
||||
<div className="bg-white/80 backdrop-blur-lg rounded-2xl border border-gray-200/50 shadow-lg overflow-hidden">
|
||||
<div className="divide-y divide-gray-200">
|
||||
<button className="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-xl">🔔</span>
|
||||
<span className="font-medium text-gray-800">Уведомления</span>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-xl">🌙</span>
|
||||
<span className="font-medium text-gray-800">Темная тема</span>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-xl">🌍</span>
|
||||
<span className="font-medium text-gray-800">Язык</span>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-xl">📊</span>
|
||||
<span className="font-medium text-gray-800">Экспорт данных</span>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logout Button */}
|
||||
<div className="px-4 mb-8">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full bg-red-500 hover:bg-red-600 text-white font-semibold py-4 px-6 rounded-2xl transition-all duration-200 transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
Выйти из аккаунта
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
@ -1,25 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect } from "react"
|
||||
import { useIonRouter } from '@ionic/react';
|
||||
import type React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useHistory } from "react-router-dom"
|
||||
import manImage from '../assets/man.svg';
|
||||
import emblemImage from '../assets/emblem.png';
|
||||
|
||||
const Welcome: React.FC = () => {
|
||||
const router = useIonRouter()
|
||||
const history = useHistory()
|
||||
const [animationPhase, setAnimationPhase] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
router.push("/home", "forward")
|
||||
}, 5000) // Задержка 3 секунды
|
||||
const timer1 = setTimeout(() => setAnimationPhase(1), 500)
|
||||
const timer2 = setTimeout(() => setAnimationPhase(2), 1500)
|
||||
const timer3 = setTimeout(() => setAnimationPhase(3), 2500)
|
||||
// const timer4 = setTimeout(() => history.push("/login"), 4000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [router])
|
||||
return () => {
|
||||
clearTimeout(timer1)
|
||||
clearTimeout(timer2)
|
||||
clearTimeout(timer3)
|
||||
// clearTimeout(timer4)
|
||||
}
|
||||
}, [history])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gradient-to-r from-teal-500 to-cyan-500">
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold leading-tight text-center text-gray-800 max-w-2xl transition-transform duration-300 ease-in-out">
|
||||
Восстановление<br />начинается здесь
|
||||
</h1>
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
|
||||
<img
|
||||
className="h-[30rem] lg:h-[50rem] w-auto z-50 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-15"
|
||||
src={manImage}
|
||||
alt=""
|
||||
/>
|
||||
{/* <img
|
||||
className="h-[30rem] lg:h-[0rem] w-auto z-50 absolute right-0 bottom-0 transform opacity-10"
|
||||
src={emblemImage}
|
||||
alt=""
|
||||
/> */}
|
||||
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#3ABBC7] to-[#0D212C]">
|
||||
{/* Floating particles */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-20 left-10 w-32 h-32 bg-white/5 rounded-full animate-pulse blur-xl"></div>
|
||||
<div className="absolute bottom-32 right-16 w-24 h-24 bg-white/10 rounded-full animate-bounce blur-lg"></div>
|
||||
<div className="absolute top-1/2 left-1/4 w-16 h-16 bg-white/5 rounded-full animate-ping blur-md"></div>
|
||||
<div className="absolute top-1/3 right-1/3 w-20 h-20 bg-white/5 rounded-full animate-pulse blur-lg"></div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
|
||||
|
||||
|
||||
<div className="text-center z-10 px-8">
|
||||
|
||||
{/* App Name */}
|
||||
<div
|
||||
className={`transition-all duration-1000 delay-300 ${animationPhase >= 1 ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
}`}
|
||||
>
|
||||
|
||||
|
||||
|
||||
<h1 className="text-5xl font-black text-white mb-4 tracking-tight filter drop-shadow-lg">Реабилитация</h1>
|
||||
<p className="text-white/90 text-xl font-medium tracking-wide">Восстановление через движение</p>
|
||||
</div>
|
||||
{/* Loading indicator */}
|
||||
<div
|
||||
className={`mt-16 transition-all duration-500 delay-700 ${animationPhase >= 1 ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
<div className="w-64 h-3 bg-white/10 rounded-full mx-auto overflow-hidden backdrop-blur-sm border border-white/20">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-white via-white/80 to-white rounded-full shadow-lg"
|
||||
style={{
|
||||
width: `${Math.min((animationPhase + 1) * 25, 100)}%`,
|
||||
transition: "width 0.8s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-white/70 text-lg mt-6 font-medium">Загрузка...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -3,52 +3,12 @@ const defaultConfig = require("shadcn/ui/tailwind.config")
|
||||
|
||||
module.exports = {
|
||||
...defaultConfig,
|
||||
content: [
|
||||
...defaultConfig.content,
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
...defaultConfig.theme,
|
||||
extend: {
|
||||
...defaultConfig.theme.extend,
|
||||
animation: {
|
||||
"gradient-x": "gradient-x 8s ease infinite",
|
||||
float: "float 3s ease-in-out infinite",
|
||||
"glow-pulse": "glow-pulse 2s ease-in-out infinite",
|
||||
},
|
||||
keyframes: {
|
||||
"gradient-x": {
|
||||
"0%, 100%": {
|
||||
"background-size": "200% 200%",
|
||||
"background-position": "left center",
|
||||
},
|
||||
"50%": {
|
||||
"background-size": "200% 200%",
|
||||
"background-position": "right center",
|
||||
},
|
||||
},
|
||||
float: {
|
||||
"0%, 100%": { transform: "translateY(0px)" },
|
||||
"50%": { transform: "translateY(-4px)" },
|
||||
},
|
||||
"glow-pulse": {
|
||||
"0%, 100%": {
|
||||
opacity: "0.5",
|
||||
transform: "scale(1)",
|
||||
},
|
||||
"50%": {
|
||||
opacity: "1",
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
},
|
||||
},
|
||||
backdropBlur: {
|
||||
xl: "24px",
|
||||
},
|
||||
},
|
||||
},
|
||||
content: [
|
||||
...defaultConfig.content,
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
plugins: [...defaultConfig.plugins, require("tailwindcss-animate")],
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user