diff --git a/src/App.tsx b/src/App.tsx index 1b9bf7d..b495b3e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,34 +1,27 @@ -import React from 'react'; -import { IonApp, IonRouterOutlet } from '@ionic/react'; -import { IonReactRouter } from '@ionic/react-router'; - -import AppRoutes from './AppRoutes'; - -import './index.css' -import '@ionic/react/css/core.css'; - -import Header from './components/Header'; -import Footer from './components/Footer'; +import type React from "react" +import { IonApp, IonRouterOutlet } from "@ionic/react" +import { IonReactRouter } from "@ionic/react-router" +import AppRoutes from "./AppRoutes" +import "./index.css" +import "@ionic/react/css/core.css" +import Header from "./components/Header" +import Footer from "./components/Footer" const App: React.FC = () => ( - -
-
- - {/* Контент с отступом равным высоте Header */} -
- - - -
- -
-
- +
+
+ {/* Контент с отступом равным высоте Header */} +
+ + + +
+
-); +) -export default App; \ No newline at end of file +export default App \ No newline at end of file diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index ef93dfd..993644f 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -1,15 +1,15 @@ -import { Route } from 'react-router-dom'; - -import Home from './pages/Home'; -import Login from './pages/Login'; -import Register from './pages/Register'; +import { Route } from "react-router-dom" +import Home from "./pages/Home" +import Login from "./pages/Login" +import Register from "./pages/Register" const AppRoutes = () => ( <> + } /> -); +) -export default AppRoutes; \ No newline at end of file +export default AppRoutes diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 30af422..5d4fa88 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,12 +1,176 @@ -import { IonRouterLink } from '@ionic/react'; +"use client" -function Footer() { - return ( -
- {/* Можно добавить содержимое футера */} - Футер -
- ); +import { useState } from "react" + +type TabType = "home" | "courses" | "profile" + +interface FooterProps { + onTabChange?: (tab: TabType) => void + activeTab?: TabType } -export default Footer; \ No newline at end of file +// Строгие и аккуратные SVG иконки +const HomeIcon = ({ filled = false, className = "" }) => ( + + {filled ? ( + + ) : ( + + )} + +) + +const BookIcon = ({ filled = false, className = "" }) => ( + + {filled ? ( + + ) : ( + + )} + +) + +const UserIcon = ({ filled = false, className = "" }) => ( + + {filled ? ( + + ) : ( + + )} + +) + +function Footer({ onTabChange, activeTab: controlledActiveTab }: FooterProps) { + const [internalActiveTab, setInternalActiveTab] = useState("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 ( +
+ {/* Основной контейнер */} +
+ {/* Фон с размытием */} +
+ + {/* Тонкая верхняя линия */} +
+ + {/* Контент */} +
+
+
+ {tabs.map((tab) => { + const isActive = activeTab === tab.id + const Icon = tab.icon + + return ( + + ) + })} +
+
+
+ + {/* Нижний отступ для мобильных устройств */} +
+
+
+ ) +} + +export default Footer diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a97c857..4a22a6e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,17 +1,161 @@ -import { IonRouterLink } from '@ionic/react'; +"use client" -function Header() { - return ( -
- - Вмеда - +import { useState } from "react" - - Зарегистрироваться - -
- ); +interface HeaderProps { + onMenuToggle?: () => void + isMenuOpen?: boolean } -export default Header; \ No newline at end of file +function Header({ onMenuToggle, isMenuOpen = false }: HeaderProps) { + const [internalMenuOpen, setInternalMenuOpen] = useState(false) + + const menuOpen = isMenuOpen || internalMenuOpen + + const handleMenuToggle = () => { + if (onMenuToggle) { + onMenuToggle() + } else { + setInternalMenuOpen(!internalMenuOpen) + } + } + + return ( +
+ {/* Основной контейнер */} +
+ {/* Фон с размытием */} +
+ + {/* Тонкая нижняя линия */} +
+ + {/* Контент */} +
+
+ {/* Логотип */} +
+ {/* Иконка логотипа */} +
+ + + +
+ + {/* Текст логотипа */} +
+

+ МедРеабилитация +

+

Центр восстановления

+
+
+ + {/* Меню-бургер */} + +
+
+
+ + {/* Выпадающее меню */} + {menuOpen && ( +
+ {/* Фон меню */} +
+ + {/* Контент меню */} +
+
+ + + {/* Разделитель */} +
+ + {/* Дополнительные опции */} +
+
+
+ И +
+
+

Иван Петров

+

Пациент

+
+
+ +
+
+
+
+ )} +
+ ) +} + +export default Header diff --git a/src/index.css b/src/index.css index a461c50..7463d24 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,193 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +/* 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); +} + +/* Кастомные стили для медицинского приложения */ +.medical-gradient { + background: linear-gradient(135deg, #0f766e 0%, #06b6d4 100%); +} + +.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); +} + +.exercise-timer { + background: linear-gradient(135deg, #0f766e, #06b6d4); + color: white; + border-radius: 16px; + padding: 1rem; +} + +/* Анимации для таймера */ +@keyframes pulse-medical { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.8; + transform: scale(1.05); + } +} + +.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; + } + + .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); +} + +/* Кастомные скроллбары */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: rgba(148, 163, 184, 0.1); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: rgba(15, 118, 110, 0.3); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(15, 118, 110, 0.5); +} + diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 69848e6..e818b06 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,13 +1,263 @@ +"use client" -function Home() { - return ( - <> -
-

Домашняя страница

-
+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" - - ); +interface Exercise { + id: number + name: string + duration: number // в секундах + description: string + difficulty: "easy" | "medium" | "hard" + completed: boolean } -export default Home; \ No newline at end of file +const Home: React.FC = () => { + const [currentExercise, setCurrentExercise] = useState(null) + const [timeLeft, setTimeLeft] = useState(0) + const [isRunning, setIsRunning] = useState(false) + const [isPaused, setIsPaused] = useState(false) + + const exercises: Exercise[] = [ + { + id: 1, + name: "Разминка плечевого сустава", + duration: 300, // 5 минут + description: "Медленные круговые движения плечами для восстановления подвижности", + difficulty: "easy", + completed: true, + }, + { + id: 2, + name: "Упражнения для кисти", + duration: 600, // 10 минут + description: "Сжимание и разжимание кулака, вращения кистью", + difficulty: "medium", + completed: false, + }, + { + id: 3, + name: "Растяжка мышц руки", + duration: 450, // 7.5 минут + description: "Статические упражнения на растяжку поврежденных мышц", + difficulty: "hard", + completed: false, + }, + ] + + 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 ( + + +
+ {/* Приветствие и статистика */} +
+

Добро пожаловать, Иван!

+

Продолжайте восстановление. Вы на правильном пути!

+
+ + {/* Общий прогресс */} + + + + + Общий прогресс + + + +
+
+ Выполнено упражнений + + {completedCount}/{totalCount} + +
+ +
{progressPercentage.toFixed(0)}% завершено
+
+
+
+ + {/* Текущее упражнение (если активно) */} + {currentExercise && ( + + + + + Текущее упражнение + + + +
+

{currentExercise.name}

+
+
{formatTime(timeLeft)}
+ +
+
+ + + {isPaused ? "Продолжить" : "Пауза"} + + + + Остановить + +
+
+
+
+ )} + + {/* Список упражнений */} +
+

+ + Сегодняшние упражнения +

+ + {exerciseList.map((exercise) => ( + + +
+
+

+ {exercise.name} +

+

{exercise.description}

+
+ {exercise.completed && ( + + Выполнено + + )} +
+ +
+
+ {formatTime(exercise.duration)} + + {exercise.difficulty === "easy" + ? "Легко" + : exercise.difficulty === "medium" + ? "Средне" + : "Сложно"} + +
+ + {!exercise.completed && !currentExercise && ( + startExercise(exercise)} + className="bg-gradient-to-r from-teal-500 to-cyan-500" + > + + Начать + + )} +
+
+
+ ))} +
+
+
+
+ ) +} + +export default Home diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index e84d66d..0b38209 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,10 +1,105 @@ -function Login() { - return ( -
-

Войти/залогиниться

+"use client" + +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" + +const Login: React.FC = () => { + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + + const handleLogin = () => { + // Логика входа + console.log("Login:", { email, password }) + } + + return ( + + +
+
+ {/* Логотип */} +
+
+ + + +
+

+ РеабилитацияПро +

+

Восстановление после травм

+
+ + {/* Форма входа */} + + +
+ + + + Email + + setEmail(e.detail.value!)} + placeholder="your@email.com" + className="text-slate-800" + /> + + + + + + Пароль + + setPassword(e.detail.value!)} + placeholder="••••••••" + className="text-slate-800" + /> + + + + + Войти + + +
+ + Нет аккаунта?{" "} + + Зарегистрироваться + + +
+
+
+
+
- ); +
+
+ ) } -export default Login; - +export default Login diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 6d2a1f3..5247a20 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,9 +1,169 @@ -function Auth() { +"use client" + +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" + +const Register: React.FC = () => { + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + password: "", + confirmPassword: "", + injuryType: "", + }) + + const handleRegister = () => { + // Логика регистрации + console.log("Register:", formData) + } + + const updateField = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })) + } + return ( -
-

Страница регистрации

-
- ); + + +
+
+ {/* Заголовок */} +
+

Регистрация пациента

+

Создайте аккаунт для начала реабилитации

+
+ + {/* Форма регистрации */} + + +
+ + + + Имя + + updateField("firstName", e.detail.value!)} + placeholder="Иван" + className="text-slate-800" + /> + + + + + + Фамилия + + updateField("lastName", e.detail.value!)} + placeholder="Петров" + className="text-slate-800" + /> + + + + + + Email + + updateField("email", e.detail.value!)} + placeholder="your@email.com" + className="text-slate-800" + /> + + + + + + Тип травмы + + updateField("injuryType", e.detail.value)} + placeholder="Выберите тип травмы" + className="text-slate-800" + > + Травма руки + Травма ноги + Травма спины + Травма плеча + Другое + + + + + + + Пароль + + updateField("password", e.detail.value!)} + placeholder="••••••••" + className="text-slate-800" + /> + + + + + + Подтвердите пароль + + updateField("confirmPassword", e.detail.value!)} + placeholder="••••••••" + className="text-slate-800" + /> + + + + + Зарегистрироваться + + +
+ + Уже есть аккаунт?{" "} + + Войти + + +
+
+
+
+
+
+
+
+ ) } -export default Auth; \ No newline at end of file +export default Register diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..c6ca9e6 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,54 @@ +/** @type {import('tailwindcss').Config} */ +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", + }, + }, + }, + plugins: [...defaultConfig.plugins, require("tailwindcss-animate")], +}