add register/login/database on aws

This commit is contained in:
oleh 2025-02-09 16:50:33 +01:00
parent b16524e94a
commit 771a6f8432
9 changed files with 836 additions and 207 deletions

View File

@ -1,19 +1,144 @@
import time
# Сохраняем оригинальную функцию time.time
_real_time = time.time
# Переопределяем time.time для смещения времени на 1 секунду назад
time.time = lambda: _real_time() - 1
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from flask_cors import CORS from flask_cors import CORS
from google.oauth2 import id_token
from google.auth.transport import requests
import logging import logging
# Импортируем функцию обработки из model.py # Импортируем функцию обработки из model.py
from model import process_query_with_mistral from model import process_query_with_mistral
import psycopg2
from psycopg2.extras import RealDictCursor
# Параметры подключения
DATABASE_CONFIG = {
"dbname": "postgres",
"user": "postgres",
"password": "healthai!",
"host": "health-ai-user-db.cxeum6cmct3r.eu-west-1.rds.amazonaws.com",
"port": 5432,
}
# Подключение к базе данных
try:
conn = psycopg2.connect(**DATABASE_CONFIG)
cursor = conn.cursor(cursor_factory=RealDictCursor)
print("Подключение к базе данных успешно установлено")
except Exception as e:
print(f"Ошибка подключения к базе данных: {e}")
conn = None
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Создаем Flask приложение # Создаем Flask приложение
app = Flask(__name__) app = Flask(__name__)
CORS(app) # Разрешаем CORS для всех доменов
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})
# Маршрут для обработки запросов от фронтенда # Ваш Google Client ID
CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"
def save_user_to_db(name, email, google_id=None, password=None):
try:
cursor.execute(
"""
INSERT INTO users (name, email, google_id, password)
VALUES (%s, %s, %s, %s)
ON CONFLICT (email) DO NOTHING
""",
(name, email, google_id, password)
)
conn.commit()
print(f"User {name} ({email}) saved successfully!")
except Exception as e:
print(f"Error saving user to database: {e}")
# Эндпоинт для верификации токенов Google OAuth
@app.route('/api/verify', methods=['POST'])
def verify_token():
data = request.get_json()
token = data.get('token')
if not token:
return jsonify({'error': 'No token provided'}), 400
try:
id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
user_email = id_info.get('email')
user_name = id_info.get('name')
google_id = id_info.get('sub') # Уникальный идентификатор пользователя Google
save_user_to_db(name=user_name, email=user_email, google_id=google_id)
logger.info(f"User authenticated and saved: {user_name} ({user_email})")
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
except ValueError as e:
logger.error(f"Token verification failed: {e}")
return jsonify({'error': 'Invalid token'}), 400
# Эндпоинт для регистрации пользователя
@app.route('/api/register', methods=['POST'])
def register():
data = request.get_json()
name = data.get('name')
email = data.get('email')
password = data.get('password') # Рекомендуется хэшировать пароль
if not all([name, email, password]):
return jsonify({'error': 'All fields are required'}), 400
try:
# Проверка, существует ли пользователь с таким email
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
existing_user = cursor.fetchone()
if existing_user:
return jsonify({'error': 'User already exists'}), 409
# Сохранение пользователя в базу данных
save_user_to_db(name=name, email=email, password=password)
return jsonify({'message': 'User registered successfully'}), 201
except Exception as e:
return jsonify({'error': str(e)}), 500
# Эндпоинт для логина пользователя (см. предыдущий пример)
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
if not all([email, password]):
return jsonify({'error': 'Email and password are required'}), 400
try:
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
user = cursor.fetchone()
if not user:
return jsonify({'error': 'Invalid credentials'}), 401
# Сравнение простым текстом — в production используйте хэширование!
if user.get('password') != password:
return jsonify({'error': 'Invalid credentials'}), 401
return jsonify({
'message': 'Login successful',
'user': {
'name': user.get('name'),
'email': user.get('email')
}
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
# Эндпоинт для обработки запросов от фронтенда
@app.route('/api/chat', methods=['POST']) @app.route('/api/chat', methods=['POST'])
def chat(): def chat():
data = request.get_json() data = request.get_json()

View File

@ -11,9 +11,9 @@
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@gsap/react": "^2.1.1", "@gsap/react": "^2.1.1",
"@material-ui/icons": "^4.11.3", "@mui/icons-material": "^6.4.3",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5", "@mui/material": "^6.1.5",
"@react-oauth/google": "^0.12.1",
"@reduxjs/toolkit": "^2.3.0", "@reduxjs/toolkit": "^2.3.0",
"appwrite": "^16.0.2", "appwrite": "^16.0.2",
"final-form": "^4.20.10", "final-form": "^4.20.10",
@ -379,14 +379,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@emotion/cache": { "node_modules/@emotion/cache": {
"version": "11.13.1", "version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@emotion/memoize": "^0.9.0", "@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0", "@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.0", "@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0", "@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0" "stylis": "4.2.0"
} }
@ -437,15 +436,14 @@
} }
}, },
"node_modules/@emotion/serialize": { "node_modules/@emotion/serialize": {
"version": "1.3.2", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@emotion/hash": "^0.9.2", "@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0", "@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0", "@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.1", "@emotion/utils": "^1.4.2",
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
@ -494,10 +492,9 @@
} }
}, },
"node_modules/@emotion/utils": { "node_modules/@emotion/utils": {
"version": "1.4.1", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
"license": "MIT"
}, },
"node_modules/@emotion/weak-memoize": { "node_modules/@emotion/weak-memoize": {
"version": "0.4.0", "version": "0.4.0",
@ -1158,45 +1155,21 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@material-ui/icons": {
"version": "4.11.3",
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz",
"integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==",
"dependencies": {
"@babel/runtime": "^7.4.4"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"@material-ui/core": "^4.0.0",
"@types/react": "^16.8.6 || ^17.0.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/core-downloads-tracker": { "node_modules/@mui/core-downloads-tracker": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz",
"integrity": "sha512-3J96098GrC95XsLw/TpGNMxhUOnoG9NZ/17Pfk1CrJj+4rcuolsF2RdF3XAFTu/3a/A+5ouxlSIykzYz6Ee87g==", "integrity": "sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg==",
"license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/mui-org" "url": "https://opencollective.com/mui-org"
} }
}, },
"node_modules/@mui/icons-material": { "node_modules/@mui/icons-material": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.3.tgz",
"integrity": "sha512-SbxFtO5I4cXfvhjAMgGib/t2lQUzcEzcDFYiRHRufZUeMMeXuoKaGsptfwAHTepYkv0VqcCwvxtvtWbpZLAbjQ==", "integrity": "sha512-3IY9LpjkwIJVgL/SkZQKKCUcumdHdQEsJaIavvsQze2QEztBt0HJ17naToN0DBBdhKdtwX5xXrfD6ZFUeWWk8g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.7" "@babel/runtime": "^7.26.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@ -1206,7 +1179,7 @@
"url": "https://opencollective.com/mui-org" "url": "https://opencollective.com/mui-org"
}, },
"peerDependencies": { "peerDependencies": {
"@mui/material": "^6.1.5", "@mui/material": "^6.4.3",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}, },
@ -1217,22 +1190,21 @@
} }
}, },
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.3.tgz",
"integrity": "sha512-rhaxC7LnlOG8zIVYv7BycNbWkC5dlm9A/tcDUp0CuwA7Zf9B9JP6M3rr50cNKxI7Z0GIUesAT86ceVm44quwnQ==", "integrity": "sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.7", "@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.1.5", "@mui/core-downloads-tracker": "^6.4.3",
"@mui/system": "^6.1.5", "@mui/system": "^6.4.3",
"@mui/types": "^7.2.18", "@mui/types": "^7.2.21",
"@mui/utils": "^6.1.5", "@mui/utils": "^6.4.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11", "@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-is": "^18.3.1", "react-is": "^19.0.0",
"react-transition-group": "^4.4.5" "react-transition-group": "^4.4.5"
}, },
"engines": { "engines": {
@ -1245,7 +1217,7 @@
"peerDependencies": { "peerDependencies": {
"@emotion/react": "^11.5.0", "@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.1.5", "@mui/material-pigment-css": "^6.4.3",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@ -1266,13 +1238,12 @@
} }
}, },
"node_modules/@mui/private-theming": { "node_modules/@mui/private-theming": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.3.tgz",
"integrity": "sha512-FJqweqEXk0KdtTho9C2h6JEKXsOT7MAVH2Uj3N5oIqs6YKxnwBn2/zL2QuYYEtj5OJ87rEUnCfFic6ldClvzJw==", "integrity": "sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.7", "@babel/runtime": "^7.26.0",
"@mui/utils": "^6.1.5", "@mui/utils": "^6.4.3",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
}, },
"engines": { "engines": {
@ -1293,14 +1264,13 @@
} }
}, },
"node_modules/@mui/styled-engine": { "node_modules/@mui/styled-engine": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.3.tgz",
"integrity": "sha512-tiyWzMkHeWlOoE6AqomWvYvdml8Nv5k5T+LDwOiwHEawx8P9Lyja6ZwWPU6xljwPXYYPT2KBp1XvMly7dsK46A==", "integrity": "sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.7", "@babel/runtime": "^7.26.0",
"@emotion/cache": "^11.13.1", "@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.2", "@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0", "@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
@ -1327,16 +1297,15 @@
} }
}, },
"node_modules/@mui/system": { "node_modules/@mui/system": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.3.tgz",
"integrity": "sha512-vPM9ocQ8qquRDByTG3XF/wfYTL7IWL/20EiiKqByLDps8wOmbrDG9rVznSE3ZbcjFCFfMRMhtxvN92bwe/63SA==", "integrity": "sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.7", "@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.1.5", "@mui/private-theming": "^6.4.3",
"@mui/styled-engine": "^6.1.5", "@mui/styled-engine": "^6.4.3",
"@mui/types": "^7.2.18", "@mui/types": "^7.2.21",
"@mui/utils": "^6.1.5", "@mui/utils": "^6.4.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
@ -1367,10 +1336,9 @@
} }
}, },
"node_modules/@mui/types": { "node_modules/@mui/types": {
"version": "7.2.18", "version": "7.2.21",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
"integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}, },
@ -1381,17 +1349,16 @@
} }
}, },
"node_modules/@mui/utils": { "node_modules/@mui/utils": {
"version": "6.1.5", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.3.tgz",
"integrity": "sha512-vp2WfNDY+IbKUIGg+eqX1Ry4t/BilMjzp6p9xO1rfqpYjH1mj8coQxxDfKxcQLzBQkmBJjymjoGOak5VUYwXug==", "integrity": "sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.7", "@babel/runtime": "^7.26.0",
"@mui/types": "^7.2.18", "@mui/types": "^7.2.21",
"@types/prop-types": "^15.7.13", "@types/prop-types": "^15.7.14",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-is": "^18.3.1" "react-is": "^19.0.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@ -1469,6 +1436,15 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@react-oauth/google": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz",
"integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.3.0.tgz",
@ -1792,10 +1768,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.13", "version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
"license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.12", "version": "18.3.12",
@ -1818,11 +1793,10 @@
} }
}, },
"node_modules/@types/react-transition-group": { "node_modules/@types/react-transition-group": {
"version": "4.4.11", "version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT", "peerDependencies": {
"dependencies": {
"@types/react": "*" "@types/react": "*"
} }
}, },
@ -2433,7 +2407,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -4099,10 +4072,9 @@
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.3.1", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
"license": "MIT"
}, },
"node_modules/react-redux": { "node_modules/react-redux": {
"version": "9.1.2", "version": "9.1.2",

View File

@ -13,9 +13,9 @@
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@gsap/react": "^2.1.1", "@gsap/react": "^2.1.1",
"@material-ui/icons": "^4.11.3", "@mui/icons-material": "^6.4.3",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5", "@mui/material": "^6.1.5",
"@react-oauth/google": "^0.12.1",
"@reduxjs/toolkit": "^2.3.0", "@reduxjs/toolkit": "^2.3.0",
"appwrite": "^16.0.2", "appwrite": "^16.0.2",
"final-form": "^4.20.10", "final-form": "^4.20.10",

View File

@ -2,6 +2,8 @@ import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom
import Navigation from './Components/Navigation'; import Navigation from './Components/Navigation';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import LandingPage from './pages/LandingPage'; import LandingPage from './pages/LandingPage';
import RegistrationForm from "./Components/RegistrationForm.tsx";
import LoginForm from "./Components/LoginForm.tsx";
const Layout = () => ( const Layout = () => (
@ -21,6 +23,8 @@ function App() {
<Router> <Router>
<Routes> <Routes>
<Route path='/' element={<LandingPage />} /> <Route path='/' element={<LandingPage />} />
<Route path="/register" element={<RegistrationForm />} />
<Route path="/login" element={<LoginForm />} />
<Route path="solutions" element={<>Sorry not implemented yet</>} /> <Route path="solutions" element={<>Sorry not implemented yet</>} />
<Route path="contact" element={<>Sorry not implemented yet</>} /> <Route path="contact" element={<>Sorry not implemented yet</>} />
<Route path="about" element={<>Sorry not implemented yet</>} /> <Route path="about" element={<>Sorry not implemented yet</>} />

View File

@ -0,0 +1,190 @@
import React from 'react';
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
import { Link, useNavigate } from 'react-router-dom';
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
const LoginFormContent: React.FC = () => {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Приведение типов для получения значений инпутов
const emailElement = document.getElementById('email') as HTMLInputElement | null;
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
if (!emailElement || !passwordElement) {
console.error('Один или несколько инпутов отсутствуют');
return;
}
const email = emailElement.value;
const password = passwordElement.value;
try {
const response = await fetch('http://localhost:5000/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok) {
console.log('Login successful:', data.message);
const loggedInUser = {
name: data.user.name,
email: data.user.email,
picture: data.user.picture || 'https://via.placeholder.com/150',
};
localStorage.setItem('user', JSON.stringify(loggedInUser));
navigate('/dashboard');
} else {
console.error('Ошибка:', data.error);
}
} catch (error) {
console.error('Ошибка при входе:', error);
}
};
const handleGoogleLoginSuccess = async (response: any) => {
try {
const res = await fetch('http://localhost:5000/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: response.credential }),
});
const data = await res.json();
const loggedInUser = {
name: data.user.name,
email: data.user.email,
picture: data.user.picture || 'https://via.placeholder.com/150',
};
localStorage.setItem('user', JSON.stringify(loggedInUser));
navigate('/dashboard');
} catch (error) {
console.error('Ошибка верификации токена:', error);
}
};
const handleGoogleLoginError = (error: any) => {
console.error('Ошибка аутентификации через Google:', error);
};
return (
<div
style={{
background: 'rgba(0, 0, 0, 0.5)',
minHeight: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '16px',
}}
>
<div
className="login-card"
style={{
maxWidth: '400px',
width: '100%',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
backgroundColor: '#fff',
padding: '24px',
}}
>
<h2 style={{ textAlign: 'center', fontWeight: 'bold', color: '#333', marginBottom: '16px' }}>
Sign In
</h2>
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Email
</label>
<input
type="email"
id="email"
required
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Password
</label>
<input
type="password"
id="password"
required
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
}}
/>
</div>
<button
type="submit"
style={{
width: '100%',
padding: '12px',
backgroundColor: '#007bff',
color: '#fff',
fontWeight: 'bold',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
marginBottom: '16px',
}}
>
Sign In
</button>
</form>
<div
style={{
textAlign: 'center',
marginBottom: '16px',
color: '#555',
fontSize: '14px',
borderBottom: '1px solid #ccc',
lineHeight: '0.1em',
margin: '10px 0 20px',
}}
>
<span style={{ background: '#fff', padding: '0 10px' }}>OR</span>
</div>
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<GoogleLogin
onSuccess={handleGoogleLoginSuccess}
onError={handleGoogleLoginError}
size="large"
width="100%"
/>
</div>
<p style={{ textAlign: 'center', color: '#555', marginTop: '16px' }}>
Don&apos;t have an account?{' '}
<Link to="/register" style={{ color: '#007bff', textDecoration: 'none' }}>
Sign Up
</Link>
</p>
</div>
</div>
);
};
const LoginForm: React.FC = () => {
return (
<GoogleOAuthProvider clientId={CLIENT_ID}>
<LoginFormContent />
</GoogleOAuthProvider>
);
};
export default LoginForm;

View File

@ -5,15 +5,16 @@ import { Link } from 'react-router-dom';
import { MdOutlineDarkMode } from "react-icons/md"; import { MdOutlineDarkMode } from "react-icons/md";
import { CiLight } from "react-icons/ci"; import { CiLight } from "react-icons/ci";
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Avatar from '@mui/material/Avatar';
import { CgLogIn } from "react-icons/cg";
import BackImage from '../assets/smallheadicon.png' import BackImage from '../assets/smallheadicon.png'
export interface NavigationItem { export interface NavigationItem {
icon: React.ReactNode, icon: React.ReactNode,
title: string title: string,
link: string link: string
} }
const NavigationItems: NavigationItem[] = [ const NavigationItems: NavigationItem[] = [
{ {
title: 'Dashboard', title: 'Dashboard',
@ -27,15 +28,11 @@ const NavigationItems: NavigationItem[] = [
} }
] ]
interface NavigationProps { interface NavigationProps {
isExpanded: boolean, isExpanded: boolean,
} }
const Navigation = ({ isExpanded = false }: NavigationProps) => { const Navigation = ({ isExpanded = false }: NavigationProps) => {
const [theme, setTheme] = useState<'dark' | 'light'>('light') const [theme, setTheme] = useState<'dark' | 'light'>('light')
useEffect(() => { useEffect(() => {
@ -58,36 +55,47 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
setTheme(theme === "dark" ? "light" : "dark") setTheme(theme === "dark" ? "light" : "dark")
} }
// Загружаем данные пользователя из localStorage (если имеются)
const [user, setUser] = useState<any>(null);
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);
return ( return (
<div className='h-full p-3 w-fit'> <div className='h-full p-3 w-fit'>
<div className='h-full rounded-xl border flex flex-col px-1 justify-between py-2 items-center dark:bg-slate-300 shadow-xl'> <div className='h-full rounded-xl border flex flex-col px-1 justify-between py-2 items-center dark:bg-slate-300 shadow-xl'>
<div className='flex flex-col items-start gap-12'> <div className='flex flex-col items-start gap-12'>
<Link to='/' className='w-full flex items-center justify-center' > <Link to='/' className='w-full flex items-center justify-center'>
<IconButton sx={{ <IconButton sx={{ width: 40, height: 40 }}>
width: 40,
height: 40,
}} >
<img src={BackImage} width={25} alt="" /> <img src={BackImage} width={25} alt="" />
</IconButton> </IconButton>
{isExpanded && <p className='text-2xl font-semibold text-dark-blue flex items-center' >Health AI</p>} {isExpanded && (
<p className='text-2xl font-semibold text-dark-blue flex items-center'>
Health AI
</p>
)}
</Link> </Link>
<div className='flex flex-col p-1 gap-5 items-center'> <div className='flex flex-col p-1 gap-5 items-center'>
{NavigationItems.map((item) => ( {NavigationItems.map((item) => (
<Link key={item.link} to={item.link} className='flex gap-2 items-center w-full'> <Link key={item.link} to={item.link} className='flex gap-2 items-center w-full'>
<IconButton sx={{ <IconButton
width: 40, sx={{
height: 40, width: 40,
borderRadius: 2, height: 40,
backgroundColor: '#FFFFFF', borderRadius: 2,
...(theme === 'dark' && { backgroundColor: '#FFFFFF',
'&:hover': { ...(theme === 'dark' && {
backgroundColor: '#eef3f4', '&:hover': {
borderColor: '#0062cc', backgroundColor: '#eef3f4',
boxShadow: 'none', borderColor: '#0062cc',
}, boxShadow: 'none',
}), },
}} > }),
}}
>
{item.icon} {item.icon}
</IconButton> </IconButton>
{isExpanded && item.title} {isExpanded && item.title}
@ -95,27 +103,46 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
))} ))}
</div> </div>
</div> </div>
<button onClick={handleThemeSwitch} className='flex items-center gap-2'> {/* Блок с иконкой пользователя и переключателем темы */}
<IconButton <div className="flex flex-col items-center gap-2">
sx={{ <Link to={user ? '/profile' : '/login'} className="flex items-center">
width: 40, <IconButton
height: 40, sx={{
borderRadius: 2, width: 40,
background: theme === 'dark' ? 'white' : 'initial', height: 40,
'&:focus-visible': { borderRadius: 2,
outline: '2px solid blue', // Кастомний стиль фокуса backgroundColor: '#FFFFFF',
outlineOffset: '0px', // Щоб межі виділення були близько до кнопки }}
borderRadius: '4px', // Залишає квадратні кути навколо фокуса >
}, {user ? (
}}> <Avatar alt={user.name} src={user.picture} />
{theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />} ) : (
</IconButton> <CgLogIn size={24} />
{isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')} )}
</button> </IconButton>
</Link>
<button onClick={handleThemeSwitch} className='flex items-center gap-2'>
<IconButton
sx={{
width: 40,
height: 40,
borderRadius: 2,
background: theme === 'dark' ? 'white' : 'initial',
'&:focus-visible': {
outline: '2px solid blue', // Кастомный стиль фокуса
outlineOffset: '0px',
borderRadius: '4px',
},
}}
>
{theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}
</IconButton>
{isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}
</button>
</div>
</div> </div>
</div> </div>
) )
} }
export default Navigation export default Navigation;

View File

@ -0,0 +1,243 @@
import React from 'react';
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
import { Link, useNavigate } from 'react-router-dom';
import gsap from 'gsap';
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
const RegistrationFormContent: React.FC = () => {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Приведение типов для получения значений инпутов
const nameElement = document.getElementById('name') as HTMLInputElement | null;
const emailElement = document.getElementById('email') as HTMLInputElement | null;
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
const confirmPasswordElement = document.getElementById('confirm-password') as HTMLInputElement | null;
if (!nameElement || !emailElement || !passwordElement || !confirmPasswordElement) {
console.error('One or more input fields are missing');
return;
}
const name = nameElement.value;
const email = emailElement.value;
const password = passwordElement.value;
const confirmPassword = confirmPasswordElement.value;
// Проверка совпадения паролей
if (password !== confirmPassword) {
console.error('Passwords do not match');
alert('Passwords do not match');
return;
}
try {
const response = await fetch('http://localhost:5000/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password }),
});
const data = await response.json();
if (response.ok) {
console.log('User registered successfully:', data.message);
// Создаем объект пользователя для авторизации (placeholder для аватара)
const loggedInUser = {
name,
email,
picture: 'https://via.placeholder.com/150',
};
localStorage.setItem('user', JSON.stringify(loggedInUser));
navigate('/dashboard');
} else {
console.error('Error:', data.error);
alert(data.error); // Показываем сообщение об ошибке, например "User already exists"
}
} catch (error) {
console.error('Error registering user:', error);
alert('Error registering user');
}
};
const handleGoogleLoginSuccess = async (response: any) => {
try {
const res = await fetch('http://localhost:5000/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: response.credential }),
});
const data = await res.json();
const loggedInUser = {
name: data.user.name,
email: data.user.email,
picture: data.user.picture || 'https://via.placeholder.com/150',
};
localStorage.setItem('user', JSON.stringify(loggedInUser));
navigate('/dashboard');
} catch (error) {
console.error('Ошибка верификации токена:', error);
}
};
const handleGoogleLoginError = (error: any) => {
console.error('Ошибка аутентификации:', error);
};
return (
<div
style={{
background: 'rgba(0, 0, 0, 0.5)',
minHeight: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '16px',
}}
>
<div
className="registration-card"
style={{
maxWidth: '400px',
width: '100%',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
backgroundColor: '#fff',
padding: '24px',
}}
>
<h2 style={{ textAlign: 'center', fontWeight: 'bold', color: '#333', marginBottom: '16px' }}>
Create Your Account
</h2>
<p style={{ textAlign: 'center', color: '#555', marginBottom: '24px' }}>
Join us to explore personalized health solutions.
</p>
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="name" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Name
</label>
<input
type="text"
id="name"
required
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Email
</label>
<input
type="email"
id="email"
required
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Password
</label>
<input
type="password"
id="password"
required
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
}}
/>
</div>
<div style={{ marginBottom: '24px' }}>
<label
htmlFor="confirm-password"
style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
>
Confirm Password
</label>
<input
type="password"
id="confirm-password"
required
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
}}
/>
</div>
<button
type="submit"
style={{
width: '100%',
padding: '12px',
backgroundColor: '#007bff',
color: '#fff',
fontWeight: 'bold',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
marginBottom: '16px',
}}
>
Register
</button>
</form>
<div
style={{
textAlign: 'center',
marginBottom: '16px',
color: '#555',
fontSize: '14px',
borderBottom: '1px solid #ccc',
lineHeight: '0.1em',
margin: '10px 0 20px',
}}
>
<span style={{ background: '#fff', padding: '0 10px' }}>OR</span>
</div>
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<GoogleLogin
onSuccess={handleGoogleLoginSuccess}
onError={handleGoogleLoginError}
size="large"
width="100%"
/>
</div>
<p style={{ textAlign: 'center', color: '#555', marginTop: '16px' }}>
Already have an account?{' '}
<Link to="/login" style={{ color: '#007bff', textDecoration: 'none' }}>
Sign In
</Link>
</p>
</div>
</div>
);
};
const RegistrationForm: React.FC = () => {
return (
<GoogleOAuthProvider clientId={CLIENT_ID}>
<RegistrationFormContent />
</GoogleOAuthProvider>
);
};
export default RegistrationForm;

View File

@ -1,13 +1,17 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { CgLogIn } from "react-icons/cg"; import { CgLogIn } from "react-icons/cg";
import { Link } from 'react-router-dom'; import BackImage from '../assets/smallheadicon.png';
import BackImage from '../assets/smallheadicon.png'
import MultiStepForm from '../Components/MultistepForm';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import { Box } from '@mui/material'; import { Box, Button, Avatar, Modal, Typography } from '@mui/material';
import gsap from 'gsap'; import gsap from 'gsap';
import { useGSAP } from '@gsap/react'; import { useGSAP } from '@gsap/react';
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
import { Link, useNavigate } from 'react-router-dom';
import RegistrationForm from "../Components/RegistrationForm";
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
// Компонент для анимации стрелки вниз
const BouncingArrow = () => { const BouncingArrow = () => {
return ( return (
<Box <Box
@ -16,8 +20,8 @@ const BouncingArrow = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mt: 2, mt: 2,
animation: 'bounce 1s infinite', // Додаємо анімацію animation: 'bounce 1s infinite',
'@keyframes bounce': { // Описуємо ключові кадри для анімації '@keyframes bounce': {
'0%, 100%': { '0%, 100%': {
transform: 'translateY(0)', transform: 'translateY(0)',
}, },
@ -31,91 +35,157 @@ const BouncingArrow = () => {
</Box> </Box>
); );
}; };
const Navbar: React.FC = () => {
interface NavbarProps {
user: any;
setUser: (user: any) => void;
}
const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
const navigate = useNavigate();
const handleSignOut = () => {
setUser(null);
localStorage.removeItem('user');
};
return ( return (
<nav className="w-full bg-white shadow-md py-4 px-2 sm:px-8 flex justify-between items-center fixed top-0 left-0 right-0 z-50"> <nav className="w-full bg-white shadow-md py-4 px-2 sm:px-8 flex justify-between items-center fixed top-0 left-0 right-0 z-50">
<div className="text-2xl font-semibold text-dark-blue flex items-center"> <div className="text-2xl font-semibold text-dark-blue flex items-center">
Health AI Health AI
<img src={BackImage} width={25} alt="" /> <img src={BackImage} width={25} alt="Logo" />
</div> </div>
<ul className="flex space-x-6 text-gray-600"> <ul className="flex space-x-6 text-gray-600">
<li><Link to="/dashboard" className="hover:text-bright-blue transition duration-300">Home</Link></li> <li>
<li><Link to="/about" className="hover:text-bright-blue transition duration-300">About</Link></li> <Link to="/dashboard" className="hover:text-bright-blue transition duration-300">
<li><Link to="/contact" className="hover:text-bright-blue transition duration-300">Contact</Link></li> Home
</Link>
</li>
<li>
<Link to="/about" className="hover:text-bright-blue transition duration-300">
About
</Link>
</li>
<li>
<Link to="/contact" className="hover:text-bright-blue transition duration-300">
Contact
</Link>
</li>
</ul> </ul>
<div className='flex gap-2 items-center'> <div className="flex items-center">
Sign in <CgLogIn size={25} /> {user ? (
<div className="flex items-center gap-2">
<Avatar alt={user.name} src={user.picture} />
<Button variant="outlined" size="small" onClick={handleSignOut}>
Sign Out
</Button>
</div>
) : (
<Button
startIcon={<CgLogIn />}
variant="outlined"
onClick={() => navigate('/register')}
>
Sign in
</Button>
)}
</div> </div>
</nav> </nav>
); );
}; };
const Home: React.FC = () => { const Home: React.FC = () => {
const navigate = useNavigate();
const [user, setUser] = useState<any>(null);
// При загрузке страницы пытаемся загрузить данные пользователя из localStorage
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);
// Анимация GSAP для элементов страницы
useGSAP(() => { useGSAP(() => {
gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 }) gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 });
gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 }) gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
gsap.from('#button', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.5, duration: 0.5 }) gsap.from('#button', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.5, duration: 0.5 });
gsap.from('#features', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.7, duration: 0.5 }) gsap.from('#features', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.7, duration: 0.5 });
gsap.from('#arrow', { opacity: 0, ease: 'power2.inOut', delay: 2, duration: 0.2 }) gsap.from('#arrow', { opacity: 0, ease: 'power2.inOut', delay: 2, duration: 0.2 });
gsap.to('#button-wrapper', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 }); gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
}, []) }, []);
// Обработчик нажатия на кнопку "Get started"
const handleGetStartedClick = () => {
if (!user) {
// Если пользователь не авторизован — переходим на страницу регистрации
navigate('/register');
} else {
// Если авторизован — переходим на страницу dashboard
navigate('/dashboard');
}
};
return ( return (
<div style={{backgroundColor: '#d0e7ff'}} className="min-h-screen"> <div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen">
<div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4"> <div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4">
<Navbar/> <Navbar user={user} setUser={setUser} />
<div className="pt-20 flex flex-col items-center"> <div className="pt-20 flex flex-col items-center">
<h1 id='mainheading' <h1
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"> id="mainheading"
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"
>
AI Assistant for Your Health AI Assistant for Your Health
</h1> </h1>
<p id='secondheading' <p
className="text-base sm:text-lg md:text-xl text-center max-w-2xl mb-8 text-gray-700"> id="secondheading"
className="text-base sm:text-lg md:text-xl text-center max-w-2xl mb-8 text-gray-700"
>
A solution for personalized health support, including tailored medication guidance and wellness A solution for personalized health support, including tailored medication guidance and wellness
insights. Take care of yourself with the power of modern technology. insights. Take care of yourself with the power of modern technology.
</p> </p>
<div className="flex flex-col sm:flex-row gap-6 mb-10" id="features"> <div className="flex flex-col sm:flex-row gap-6 mb-10" id="features">
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md"> <div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
<h3 className="text-xl font-medium mb-3 text-dark-blue">Personalized Prescription</h3> <h3 className="text-xl font-medium mb-3 text-dark-blue">Personalized Prescription</h3>
<p className="text-gray-600">Receive tailored medication recommendations specifically <p className="text-gray-600">
designed for your needs.</p> Receive tailored medication recommendations specifically designed for your needs.
</p>
</div> </div>
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md"> <div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
<h3 className="text-xl font-medium mb-3 text-dark-blue">Health Monitoring</h3> <h3 className="text-xl font-medium mb-3 text-dark-blue">Health Monitoring</h3>
<p className="text-gray-600">Stay informed about your health with real-time monitoring and <p className="text-gray-600">
AI-driven insights.</p> Stay informed about your health with real-time monitoring and AI-driven insights.
</p>
</div> </div>
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md"> <div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
<h3 className="text-xl font-medium mb-3 text-dark-blue">Advanced AI Support</h3> <h3 className="text-xl font-medium mb-3 text-dark-blue">Advanced AI Support</h3>
<p className="text-gray-600">Utilize AI support to ensure you're following the best routines <p className="text-gray-600">
for a healthier lifestyle.</p> Utilize AI support to ensure you're following the best routines for a healthier lifestyle.
</p>
</div> </div>
</div> </div>
<div id='arrow' className='flex flex-col items-center mt-10 z-0'> <div id="arrow" className="flex flex-col items-center mt-10 z-0">
<p className='text-gray-600'>Try it out</p> <p className="text-gray-600">Try it out</p>
<BouncingArrow/> <BouncingArrow />
</div> </div>
<div id="button-wrapper" className="flex justify-center opacity-0 mt-6"> <div id="button-wrapper" className="flex justify-center mt-6">
<Link id='button' to='/dashboard'> <button
<button id="button"
className="bg-bright-blue text-white font-medium py-2 px-5 rounded hover:bg-deep-blue transition duration-300 shadow-md"> onClick={handleGetStartedClick}
Get started className="bg-bright-blue text-white font-medium py-2 px-5 rounded hover:bg-deep-blue transition duration-300 shadow-md"
</button> >
</Link> Get started
</button>
</div> </div>
</div> </div>
</div> </div>
{/*<div className='w-full h-screen flex flex-col justify-center items-center'>*/}
{/* <MultiStepForm />*/}
{/*</div>*/}
<footer className="mt-auto text-center text-gray-500 p-4"> <footer className="mt-auto text-center text-gray-500 p-4">
<p>&copy; {new Date().getFullYear()} Health AI. All rights reserved.</p> <p>&copy; {new Date().getFullYear()} Health AI. All rights reserved.</p>
</footer> </footer>
@ -124,5 +194,3 @@ const Home: React.FC = () => {
}; };
export default Home; export default Home;