From 3743387c31d85c927ec60eae6b18d9083bdf4a01 Mon Sep 17 00:00:00 2001 From: Rishi Garg Date: Tue, 31 Dec 2024 12:43:31 +0530 Subject: [PATCH 1/2] Frontend Added Signed-off-by: Rishi Garg --- healthhub-frontend/package-lock.json | 70 +- healthhub-frontend/package.json | 3 +- healthhub-frontend/src/App.jsx | 113 +++ .../src/components/Allergies.jsx | 515 ++++++++++++++ .../src/components/Dashboard.jsx | 373 ++++++++++ .../src/components/EmailStep.jsx | 279 ++++++++ .../src/components/ForgotPasswordFlow.jsx | 256 +++++++ .../src/components/LoadingTransition.jsx | 284 ++++++++ .../src/components/Medications.jsx | 669 ++++++++++++++++++ .../src/components/NewPasswordStep.jsx | 404 +++++++++++ healthhub-frontend/src/components/OTPStep.jsx | 487 +++++++++++++ .../src/components/authentication.jsx | 163 +++-- .../src/components/basic_information.jsx | 327 +++++++++ .../src/components/contact_detail.jsx | 353 +++++++++ .../src/components/emergency_contact.jsx | 314 ++++++++ .../src/components/medication_details.jsx | 459 ++++++++++++ .../src/components/otp_verification.jsx | 461 ++++++++++++ healthhub-frontend/src/components/review.jsx | 431 +++++++++++ .../src/components/vital_stats.jsx | 337 +++++++++ healthhub-frontend/src/main.jsx | 20 +- 20 files changed, 6248 insertions(+), 70 deletions(-) create mode 100644 healthhub-frontend/src/App.jsx create mode 100644 healthhub-frontend/src/components/Allergies.jsx create mode 100644 healthhub-frontend/src/components/Dashboard.jsx create mode 100644 healthhub-frontend/src/components/EmailStep.jsx create mode 100644 healthhub-frontend/src/components/ForgotPasswordFlow.jsx create mode 100644 healthhub-frontend/src/components/LoadingTransition.jsx create mode 100644 healthhub-frontend/src/components/Medications.jsx create mode 100644 healthhub-frontend/src/components/NewPasswordStep.jsx create mode 100644 healthhub-frontend/src/components/OTPStep.jsx create mode 100644 healthhub-frontend/src/components/basic_information.jsx create mode 100644 healthhub-frontend/src/components/contact_detail.jsx create mode 100644 healthhub-frontend/src/components/emergency_contact.jsx create mode 100644 healthhub-frontend/src/components/medication_details.jsx create mode 100644 healthhub-frontend/src/components/otp_verification.jsx create mode 100644 healthhub-frontend/src/components/review.jsx create mode 100644 healthhub-frontend/src/components/vital_stats.jsx diff --git a/healthhub-frontend/package-lock.json b/healthhub-frontend/package-lock.json index 2d8bb16..7a1e54a 100644 --- a/healthhub-frontend/package-lock.json +++ b/healthhub-frontend/package-lock.json @@ -12,7 +12,8 @@ "framer-motion": "^11.14.4", "lucide-react": "^0.468.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -1311,6 +1312,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1772,6 +1779,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3750,6 +3766,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", + "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz", + "integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==", + "license": "MIT", + "dependencies": { + "react-router": "7.1.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz", @@ -3916,6 +3972,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -4201,6 +4263,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/healthhub-frontend/package.json b/healthhub-frontend/package.json index 3885d0c..f4137b8 100644 --- a/healthhub-frontend/package.json +++ b/healthhub-frontend/package.json @@ -14,7 +14,8 @@ "framer-motion": "^11.14.4", "lucide-react": "^0.468.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/healthhub-frontend/src/App.jsx b/healthhub-frontend/src/App.jsx new file mode 100644 index 0000000..5cd259f --- /dev/null +++ b/healthhub-frontend/src/App.jsx @@ -0,0 +1,113 @@ +import React, { useState, createContext } from 'react'; +import { Routes, Route, Navigate, useNavigate } from 'react-router-dom'; +import AuthPage from './components/authentication'; +import OTPVerification from './components/otp_verification'; +import HealthProfileForm from './components/medication_details'; +import ForgotPasswordFlow from './components/ForgotPasswordFlow'; +import Dashboard from './components/Dashboard'; +import LoadingTransition from './components/LoadingTransition'; + +export const AuthContext = createContext(null); + +const Layout = ({ children }) => ( +
+ {children} +
+); + +const ProtectedRoute = ({ children }) => { + const isAuthenticated = true; // actual auth check is required + return isAuthenticated ? children : ; +}; + +const App = () => { + const [email, setEmail] = useState(''); + const [formData, setFormData] = useState({}); + const [showLoading, setShowLoading] = useState(false); + const navigate = useNavigate(); + + const handleAuthComplete = (userEmail) => { + setEmail(userEmail); + navigate('/otp-verification'); + }; + + const handleOTPComplete = () => { + navigate('/profile'); + }; + + const handleProfileComplete = async (data) => { + try { + setFormData(data); + setShowLoading(true); + localStorage.setItem('healthProfile', JSON.stringify(data)); + } catch (error) { + console.error('Error completing profile:', error); + setShowLoading(false); + } + }; + + const handleLoadingComplete = () => { + setShowLoading(false); + navigate('/dashboard'); + }; + + return ( + + + {showLoading && } + + } /> + } + /> + } + /> + + } + /> + + } + /> + + + + } + /> + +
+

Page Not Found

+ +
+ + } /> +
+
+
+ ); + }; + + export default App; \ No newline at end of file diff --git a/healthhub-frontend/src/components/Allergies.jsx b/healthhub-frontend/src/components/Allergies.jsx new file mode 100644 index 0000000..bc12d0f --- /dev/null +++ b/healthhub-frontend/src/components/Allergies.jsx @@ -0,0 +1,515 @@ +import React, { useState, useEffect, memo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { AlertTriangle, Plus, X, Calendar, AlertCircle } from 'lucide-react'; + +const InputField = memo(({ + label, + value = '', + onChange, + error, + touched, + required = false, + placeholder, + onFocus, + onBlur, + isFocused, + type = "text" +}) => ( +
+ +
+ onChange(e.target.value)} + onFocus={onFocus} + onBlur={onBlur} + className={` + w-full px-4 py-3 rounded-xl border-2 + transition-colors duration-300 focus:outline-none + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + placeholder={placeholder} + /> + +
+ + {error && touched && ( + + + {error} + + )} + +
+
+
+)); + +const DateInput = memo(({ + label, + value = '', + onChange, + error, + touched, + onFocus, + onBlur, + isFocused +}) => ( +
+ +
+ onChange(e.target.value)} + onFocus={onFocus} + onBlur={onBlur} + max={new Date().toISOString().split('T')[0]} + className={` + w-full px-4 py-3 pl-12 rounded-xl border-2 + transition-all duration-300 focus:outline-none + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + /> + +
+
+)); + +const AllergyCard = memo(({ + allergy, + index, + onRemove, + onChange, + onFocus, + onBlur, + errors, + touched, + focusedField, + SEVERITY_LEVELS, + COMMON_REACTIONS, + handleReactionToggle +}) => ( + + onRemove(index)} + className="absolute -top-2 -right-2 bg-red-100 p-2 rounded-full text-red-500 + opacity-0 group-hover:opacity-100 transform scale-90 group-hover:scale-100 + transition-all duration-300 hover:bg-red-200" + > + + + +
+ + Allergy {index + 1} +
+ + onChange(index, 'allergen', value)} + error={errors[`allergy${index}_allergen`]} + touched={touched[`allergy${index}_allergen`]} + isFocused={focusedField === `${index}_allergen`} + onFocus={() => onFocus(`${index}_allergen`)} + onBlur={() => onBlur(`${index}_allergen`)} + required + placeholder="Enter allergen name" + /> + +
+ +
+ {SEVERITY_LEVELS.map((level) => ( + onChange(index, 'severity', level)} + className={` + px-4 py-2 rounded-xl border-2 transition-all duration-300 + ${allergy.severity === level + ? 'border-teal-500 bg-teal-50 text-teal-600' + : 'border-gray-200 hover:border-gray-300' + } + `} + > + {level} + + ))} +
+ {errors[`allergy${index}_severity`] && touched[`allergy${index}_severity`] && ( + + + {errors[`allergy${index}_severity`]} + + )} +
+ + onChange(index, 'diagnosedDate', value)} + onFocus={() => onFocus(`${index}_diagnosedDate`)} + onBlur={() => onBlur(`${index}_diagnosedDate`)} + isFocused={focusedField === `${index}_diagnosedDate`} + /> + +
+ +
+ {COMMON_REACTIONS.map((reaction) => ( + handleReactionToggle(index, reaction)} + className={` + px-4 py-2 rounded-xl border-2 transition-all duration-300 + ${allergy.reactions?.includes(reaction) + ? 'border-teal-500 bg-teal-50 text-teal-600' + : 'border-gray-200 hover:border-gray-300' + } + `} + > + {reaction} + + ))} +
+ {errors[`allergy${index}_reactions`] && touched[`allergy${index}_reactions`] && ( + + + {errors[`allergy${index}_reactions`]} + + )} +
+
+)); + +const AllergiesStep = ({ + data = { allergies: [] }, + onChange, + onValidationChange +}) => { + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [focusedField, setFocusedField] = useState(null); + + const SEVERITY_LEVELS = ['Mild', 'Moderate', 'Severe', 'Life-Threatening']; + const COMMON_REACTIONS = [ + 'Rash', 'Hives', 'Swelling', 'Difficulty Breathing', + 'Nausea', 'Anaphylaxis', 'Itching', 'Dizziness' + ]; + + const validateAllergy = (allergy, index) => { + const newErrors = {}; + + if (!allergy.allergen?.trim()) { + newErrors[`allergy${index}_allergen`] = 'Allergen name is required'; + } + + if (!allergy.severity) { + newErrors[`allergy${index}_severity`] = 'Severity level is required'; + } + + if (!allergy.reactions?.length) { + newErrors[`allergy${index}_reactions`] = 'Select at least one reaction'; + } + + return newErrors; + }; + + const handleAddAllergy = () => { + const allergies = [...(data.allergies || [])]; + allergies.push({ + allergen: '', + severity: '', + diagnosedDate: '', + reactions: [] + }); + + const updatedData = { ...data, allergies }; + onChange(updatedData); + validateAllergies(allergies); + }; + + const handleRemoveAllergy = (index) => { + const allergies = data.allergies.filter((_, i) => i !== index); + const updatedData = { ...data, allergies }; + onChange(updatedData); + validateAllergies(allergies); + }; + + const handleAllergyChange = (index, field, value) => { + const allergies = [...(data.allergies || [])]; + if (!allergies[index]) { + allergies[index] = { + allergen: '', + severity: '', + diagnosedDate: '', + reactions: [] + }; + } + + allergies[index] = { + ...allergies[index], + [field]: value + }; + + const updatedData = { ...data, allergies }; + onChange(updatedData); + + setTouched(prev => ({ + ...prev, + [`allergy${index}_${field}`]: true + })); + + validateAllergies(allergies); + }; + + const handleReactionToggle = (index, reaction) => { + const allergies = [...(data.allergies || [])]; + if (!allergies[index]) { + allergies[index] = { + allergen: '', + severity: '', + diagnosedDate: '', + reactions: [] + }; + } + + const currentReactions = allergies[index].reactions || []; + allergies[index] = { + ...allergies[index], + reactions: currentReactions.includes(reaction) + ? currentReactions.filter(r => r !== reaction) + : [...currentReactions, reaction] + }; + + const updatedData = { ...data, allergies }; + onChange(updatedData); + + setTouched(prev => ({ + ...prev, + [`allergy${index}_reactions`]: true + })); + + validateAllergies(allergies); + }; + + const validateAllergies = (allergies = []) => { + let newErrors = {}; + + allergies.forEach((allergy, index) => { + newErrors = { + ...newErrors, + ...validateAllergy(allergy, index) + }; + }); + + setErrors(newErrors); + onValidationChange(Object.keys(newErrors).length === 0); + }; + + const handleFocus = (fieldId) => { + setFocusedField(fieldId); + }; + + const handleBlur = (fieldId) => { + setFocusedField(null); + setTouched(prev => ({ + ...prev, + [`allergy${fieldId}`]: true + })); + }; + + useEffect(() => { + validateAllergies(data.allergies); + }, []); + + return ( + + + {data.allergies?.map((allergy, index) => ( + + ))} + + + + + {data.allergies?.length ? 'Add Another Allergy' : 'Add Allergy'} + + + {data.allergies?.length === 0 && ( + +
+ + No Allergies Added +
+

Add any known allergies to maintain a complete health profile.

+
+ )} + + {/* Mobile responsiveness styles */} + +
+ ); +}; + +export default memo(AllergiesStep); \ No newline at end of file diff --git a/healthhub-frontend/src/components/Dashboard.jsx b/healthhub-frontend/src/components/Dashboard.jsx new file mode 100644 index 0000000..89f6175 --- /dev/null +++ b/healthhub-frontend/src/components/Dashboard.jsx @@ -0,0 +1,373 @@ +import React, { useState } from 'react'; +import { + User, Calendar, Hospital, Sun, Moon, Menu, X, + MapPin, Clock, Bell, ChevronRight, Settings, + FileText, Heart, Activity, Plus +} from 'lucide-react'; + +const Dashboard = () => { + const [isDarkMode, setIsDarkMode] = useState(false); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const [activeTab, setActiveTab] = useState('dashboard'); + + const toggleDarkMode = () => { + setIsDarkMode(!isDarkMode); + document.documentElement.classList.toggle('dark'); + }; + + const MenuItems = [ + { id: 'dashboard', icon: Activity, label: 'Dashboard' }, + { id: 'profile', icon: User, label: 'Edit Profile' }, + { id: 'appointments', icon: Calendar, label: 'Manage Appointments' }, + { id: 'hospitals', icon: Hospital, label: 'Nearby Hospitals' }, + { id: 'records', icon: FileText, label: 'Health Records' }, + { id: 'settings', icon: Settings, label: 'Settings' } + ]; + + const appointments = [ + { id: 1, doctor: "Dr. Sarah Wilson", type: "General Checkup", date: "2024-01-15", time: "10:00 AM", status: "upcoming" }, + { id: 2, doctor: "Dr. Michael Chen", type: "Dental", date: "2024-01-18", time: "2:30 PM", status: "upcoming" }, + { id: 3, doctor: "Dr. Emily Brown", type: "Cardiology", date: "2024-01-20", time: "11:15 AM", status: "pending" } + ]; + + const hospitals = [ + { id: 1, name: "City General Hospital", distance: "1.2 km", availability: "Open" }, + { id: 2, name: "St. Mary's Medical Center", distance: "2.5 km", availability: "Open" }, + { id: 3, name: "Park View Hospital", distance: "3.8 km", availability: "Closed" } + ]; + + return ( +
+ + +
+
+
+

+ Welcome, John +

+
+ + +
+
+
+ +
+
+ {[ + { label: 'Upcoming Appointments', value: '3', icon: Calendar, color: 'blue' }, + { label: 'Nearby Hospitals', value: '8', icon: Hospital, color: 'teal' }, + { label: 'Recent Records', value: '12', icon: FileText, color: 'purple' }, + { label: 'Notifications', value: '5', icon: Bell, color: 'pink' } + ].map((stat, index) => ( +
+
+
+

+ {stat.label} +

+

+ {stat.value} +

+
+
+ +
+
+
+ ))} +
+ +
+
+
+

+ Upcoming Appointments +

+ +
+ +
+ {appointments.map(appointment => ( +
+
+
+

+ {appointment.doctor} +

+

+ {appointment.type} +

+
+
+ {appointment.status} +
+
+
+
+ + + {appointment.date} + +
+
+ + + {appointment.time} + +
+
+
+ ))} +
+
+ +
+
+

+ Nearby Hospitals +

+ +
+ +
+ {hospitals.map(hospital => ( +
+
+
+

+ {hospital.name} +

+

+ {hospital.distance} away +

+
+
+ {hospital.availability} +
+
+ +
+ ))} +
+
+
+ +
+

+ Health Summary +

+
+
+

+ Latest Vitals +

+
+ {[ + { label: 'Blood Pressure', value: '120/80 mmHg' }, + { label: 'Heart Rate', value: '72 bpm' }, + { label: 'Temperature', value: '98.6°F' }, + { label: 'Blood Sugar', value: '95 mg/dL' } + ].map((vital, index) => ( +
+ + {vital.label} + + + {vital.value} + +
+ ))} +
+
+ +
+

+ Current Medications +

+
+ {[ + { name: 'Amoxicillin', dosage: '500mg', frequency: 'Twice daily' }, + { name: 'Lisinopril', dosage: '10mg', frequency: 'Once daily' }, + { name: 'Metformin', dosage: '850mg', frequency: 'With meals' } + ].map((med, index) => ( +
+
+ {med.name} +
+
+ {med.dosage} - {med.frequency} +
+
+ ))} +
+
+ +
+

+ Upcoming Tests +

+
+ {[ + { name: 'Blood Work', date: '2024-01-20', time: '9:00 AM' }, + { name: 'X-Ray', date: '2024-01-25', time: '2:30 PM' }, + { name: 'ECG', date: '2024-02-01', time: '11:15 AM' } + ].map((test, index) => ( +
+
+ {test.name} +
+
+ {test.date} at {test.time} +
+
+ ))} +
+
+
+
+
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/healthhub-frontend/src/components/EmailStep.jsx b/healthhub-frontend/src/components/EmailStep.jsx new file mode 100644 index 0000000..86547db --- /dev/null +++ b/healthhub-frontend/src/components/EmailStep.jsx @@ -0,0 +1,279 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Mail, ArrowRight, ArrowLeft, Shield, LockKeyhole, + Heart, Activity, Calendar, X, Fingerprint, AlertCircle +} from 'lucide-react'; + +const HeartbeatLine = ({ top, opacity = 1 }) => { + return ( +
+ + + +
+ ); +}; + +const PulseCircle = ({ size = "lg", color = "white", delay = 0 }) => { + const sizes = { + sm: "h-8 w-8", + md: "h-12 w-12", + lg: "h-16 w-16" + }; + + return ( +
+
+
+
+ ); +}; + +export const EmailStep = ({ onSubmit, onBack }) => { + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [activeField, setActiveField] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const validateEmail = (email) => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validateEmail(email)) { + setError('Please enter a valid email address'); + return; + } + + setIsLoading(true); + setError(''); + + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + type: 'password_reset_request', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Failed to process request'); + } + + await response.json(); + onSubmit(email); + + } catch (error) { + setError('An error occurred. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+ +
+
+
+ +
+
+

+ HealthHub +

+
+ +
+
+

+ Secure Password Reset +

+

+ Let's help you regain access to your account +

+
+
+
+ +
+
+
+
+ {[...Array(4)].map((_, i) => ( +
+ User avatar +
+
+ ))} +
+
+
+ Join 10,000+ users +
+
+ Transform your healthcare experience today +
+
+
+
+
+
+ +
+
+
+
+
+ +

Reset Password

+
+
+ +
+
+
+ +
+ + { + setEmail(e.target.value); + setError(''); + }} + onFocus={() => setActiveField('email')} + onBlur={() => setActiveField(null)} + className="w-full pl-12 pr-4 py-4 border-2 border-gray-200 rounded-2xl + focus:ring-4 focus:ring-teal-500/20 focus:border-teal-500 + transition-all duration-300" + placeholder="Enter your email" + disabled={isLoading} + /> +
+ + + {error && ( + + + {error} + + )} + +
+ + + {isLoading ? ( +
+ ) : ( + <> + Continue + + + )} + + + + +
+ +
+
+ + Secure Password Reset +
+
+
+
+
+
+
+ ); +}; + +export default EmailStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/ForgotPasswordFlow.jsx b/healthhub-frontend/src/components/ForgotPasswordFlow.jsx new file mode 100644 index 0000000..ba29b5f --- /dev/null +++ b/healthhub-frontend/src/components/ForgotPasswordFlow.jsx @@ -0,0 +1,256 @@ +import React, { useState, useEffect } from 'react'; +import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Shield, LockKeyhole, Loader, Heart } from 'lucide-react'; +import { EmailStep } from './EmailStep'; +import { OTPStep } from './OTPStep'; +import { NewPasswordStep } from './NewPasswordStep'; + +const TransitionOverlay = ({ isVisible }) => { + const pulseAnimation = { + scale: [1, 1.1, 1], + opacity: [0.5, 1, 0.5], + transition: { + duration: 2, + repeat: Infinity, + ease: "easeInOut" + } + }; + + return ( + + {isVisible && ( + +
+
+ {[...Array(3)].map((_, i) => ( + + ))} +
+
+ +
+ +
+ + + + + + + +
+ + +

+ Please wait +

+

+ Securing your session... +

+
+
+
+
+ )} +
+ ); +}; + +const ForgotPasswordFlow = () => { + const [email, setEmail] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [showTransition, setShowTransition] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + + const handleTransition = async (callback) => { + setShowTransition(true); + setIsLoading(true); + + try { + await new Promise(resolve => setTimeout(resolve, 500)); + await callback(); + await new Promise(resolve => setTimeout(resolve, 800)); + } catch (error) { + console.error('Error during transition:', error); + } finally { + setShowTransition(false); + setIsLoading(false); + } + }; + + const handleEmailSubmit = async (email) => { + await handleTransition(async () => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + type: 'forgot_password_email', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Failed to send email'); + } + + await response.json(); + setEmail(email); + navigate('/forgot-password/verify'); + } catch (error) { + throw new Error('Failed to process email submission'); + } + }); + }; + + const handleOTPSubmit = async (otp) => { + await handleTransition(async () => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + otp, + type: 'verify_otp', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Failed to verify OTP'); + } + + await response.json(); + navigate('/forgot-password/reset'); + } catch (error) { + throw new Error('Failed to verify OTP'); + } + }); + }; + + const handlePasswordReset = async (newPassword) => { + await handleTransition(async () => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + newPassword, + type: 'reset_password', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Failed to reset password'); + } + + await response.json(); + navigate('/auth'); + } catch (error) { + throw new Error('Failed to reset password'); + } + }); + }; + + return ( + <> + + + + + navigate('/auth')} + isLoading={isLoading} + /> + } /> + + console.log('Resend OTP')} + isLoading={isLoading} + /> + } /> + + + } /> + + } /> + + + + ); +}; + +export default ForgotPasswordFlow; \ No newline at end of file diff --git a/healthhub-frontend/src/components/LoadingTransition.jsx b/healthhub-frontend/src/components/LoadingTransition.jsx new file mode 100644 index 0000000..536f2d3 --- /dev/null +++ b/healthhub-frontend/src/components/LoadingTransition.jsx @@ -0,0 +1,284 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Sparkles, Heart, Shield, Activity, CheckCircle2, + User, Settings, Database, Zap, Star, + Flame, Cloud, Waves +} from 'lucide-react'; + +const FloatingElement = ({ children, delay = 0 }) => ( + + {children} + +); + +const CircularProgress = ({ progress }) => ( + + + + +); + +const Particle = ({ index }) => { + const randomDelay = Math.random() * 2; + const size = Math.random() * 4 + 2; + + return ( + + ); +}; + +const LoadingTransition = ({ onComplete }) => { + const [currentStep, setCurrentStep] = useState(0); + const [isDone, setIsDone] = useState(false); + const [progress, setProgress] = useState(0); + + const steps = [ + { text: 'Initializing Health Profile', icon: User, color: 'text-blue-300' }, + { text: 'Configuring Dashboard', icon: Activity, color: 'text-green-300' }, + { text: 'Encrypting Data', icon: Shield, color: 'text-purple-300' }, + { text: 'Finalizing Setup', icon: Sparkles, color: 'text-yellow-300' }, + ]; + + useEffect(() => { + const timer = setTimeout(() => { + if (currentStep < steps.length - 1) { + setCurrentStep(prev => prev + 1); + } else if (!isDone) { + setIsDone(true); + setTimeout(() => { + onComplete?.(); + }, 1000); + } + }, 1500); + + return () => clearTimeout(timer); + }, [currentStep, isDone, onComplete, steps.length]); + + useEffect(() => { + const progressInterval = setInterval(() => { + setProgress(prev => (prev + 0.01) % 1); + }, 50); + + return () => clearInterval(progressInterval); + }, []); + + return ( +
+
+ {[...Array(50)].map((_, i) => ( + + ))} + + {[...Array(3)].map((_, i) => ( + + ))} +
+ +
+
+ + + + + + + + + + +
+ +
+ + {steps.map((step, index) => ( + +
+ {index < currentStep ? ( + + + + ) : ( + + + + )} + + {index === currentStep && ( + + )} +
+ + {step.text} + + {index === currentStep && ( +
+ {[...Array(3)].map((_, i) => ( + + ))} +
+ )} +
+ ))} +
+
+ +
+ + +
+ + + + +
+
+ ); +}; + +export default LoadingTransition; \ No newline at end of file diff --git a/healthhub-frontend/src/components/Medications.jsx b/healthhub-frontend/src/components/Medications.jsx new file mode 100644 index 0000000..5f8fcc2 --- /dev/null +++ b/healthhub-frontend/src/components/Medications.jsx @@ -0,0 +1,669 @@ +import React, { useState, useEffect, memo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Pill, Plus, X, Calendar, AlertCircle, Clock, User, + RefreshCcw, ThumbsUp +} from 'lucide-react'; + +const InputField = memo(({ + icon: Icon, + label, + value = '', + onChange, + error, + required = false, + placeholder, + touched, + onFocus, + onBlur, + isFocused +}) => ( +
+ +
+ onChange(e.target.value)} + onFocus={onFocus} + onBlur={onBlur} + className={` + w-full px-4 py-3 ${Icon ? 'pl-12' : ''} + rounded-xl border-2 + transition-all duration-300 focus:outline-none + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + placeholder={placeholder} + /> + {Icon && ( + + )} + + {error && touched && ( + + + {error} + + )} +
+
+)); + +const DateInput = memo(({ + label, + value = '', + onChange, + min, + max, + error, + required = false, + touched, + onFocus, + onBlur, + isFocused +}) => ( +
+ +
+ onChange(e.target.value)} + min={min} + max={max} + onFocus={onFocus} + onBlur={onBlur} + className={` + w-full px-4 py-3 pl-12 rounded-xl border-2 + transition-all duration-300 focus:outline-none + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + /> + + + {error && touched && ( + + + {error} + + )} +
+
+)); + +const SelectionGroup = memo(({ + label, + options, + selected = '', + onChange, + error, + required = false, + touched +}) => ( +
+ +
+ {options.map((option) => ( + onChange(option)} + className={` + px-4 py-2 rounded-xl border-2 transition-all duration-300 + ${selected === option + ? 'border-teal-500 bg-teal-50 text-teal-600' + : 'border-gray-200 hover:border-gray-300' + } + `} + > + {option} + + ))} +
+ + {error && touched && ( + + + {error} + + )} +
+)); + +const MedicationCard = memo(({ + medication, + index, + type = 'current', + onRemove, + onChange, + onFocus, + onBlur, + errors, + touched, + focusedField, + FREQUENCIES, + COMMON_SIDE_EFFECTS, + handleSideEffectToggle +}) => ( + + onRemove(index, type)} + className="absolute -top-2 -right-2 bg-red-100 p-2 rounded-full text-red-500 + opacity-0 group-hover:opacity-100 transform scale-90 group-hover:scale-100 + transition-all duration-300 hover:bg-red-200" + > + + + +
+ + + {type === 'current' ? 'Current' : 'Past'} Medication {index + 1} + +
+ +
+ onChange(index, 'name', value, type)} + error={errors[`${type === 'past' ? 'pastMed' : 'med'}${index}_name`]} + touched={touched[`${type === 'past' ? 'pastMed' : 'med'}${index}_name`]} + onFocus={() => onFocus(`${type}_${index}_name`)} + onBlur={() => onBlur(`${type}_${index}_name`)} + isFocused={focusedField === `${type}_${index}_name`} + required + /> + + onChange(index, 'dosage', value, type)} + error={errors[`${type === 'past' ? 'pastMed' : 'med'}${index}_dosage`]} + touched={touched[`${type === 'past' ? 'pastMed' : 'med'}${index}_dosage`]} + onFocus={() => onFocus(`${type}_${index}_dosage`)} + onBlur={() => onBlur(`${type}_${index}_dosage`)} + isFocused={focusedField === `${type}_${index}_dosage`} + required={type === 'current'} + placeholder="e.g., 50mg" + /> +
+ +
+ onChange(index, 'startDate', value, type)} + error={errors[`${type === 'past' ? 'pastMed' : 'med'}${index}_startDate`]} + touched={touched[`${type === 'past' ? 'pastMed' : 'med'}${index}_startDate`]} + onFocus={() => onFocus(`${type}_${index}_startDate`)} + onBlur={() => onBlur(`${type}_${index}_startDate`)} + isFocused={focusedField === `${type}_${index}_startDate`} + max={new Date().toISOString().split('T')[0]} + required + /> + + {type === 'past' && ( + onChange(index, 'endDate', value, type)} + error={errors[`pastMed${index}_endDate`]} + touched={touched[`pastMed${index}_endDate`]} + onFocus={() => onFocus(`${type}_${index}_endDate`)} + onBlur={() => onBlur(`${type}_${index}_endDate`)} + isFocused={focusedField === `${type}_${index}_endDate`} + min={medication?.startDate} + max={new Date().toISOString().split('T')[0]} + required + /> + )} +
+ + {type === 'current' && ( + <> + onChange(index, 'frequency', value)} + error={errors[`med${index}_frequency`]} + touched={touched[`med${index}_frequency`]} + required + /> + + onChange(index, 'prescribedBy', value)} + onFocus={() => onFocus(`${type}_${index}_prescribedBy`)} + onBlur={() => onBlur(`${type}_${index}_prescribedBy`)} + isFocused={focusedField === `${type}_${index}_prescribedBy`} + placeholder="Doctor's name" + /> + + onChange(index, 'purpose', value)} + onFocus={() => onFocus(`${type}_${index}_purpose`)} + onBlur={() => onBlur(`${type}_${index}_purpose`)} + isFocused={focusedField === `${type}_${index}_purpose`} + placeholder="What is this medication for?" + /> + +
+ +
+ {COMMON_SIDE_EFFECTS.map((effect) => ( + handleSideEffectToggle(index, effect)} + className={` + px-4 py-2 rounded-xl border-2 transition-all duration-300 + ${medication?.sideEffects?.includes(effect) + ? 'border-teal-500 bg-teal-50 text-teal-600' + : 'border-gray-200 hover:border-gray-300' + } + `} + > + {effect} + + ))} +
+
+ + )} + + {type === 'past' && ( + onChange(index, 'reason', value, type)} + onFocus={() => onFocus(`${type}_${index}_reason`)} + onBlur={() => onBlur(`${type}_${index}_reason`)} + isFocused={focusedField === `${type}_${index}_reason`} + placeholder="Why did you stop taking this medication?" + /> + )} +
+)); + +const MedicationsStep = memo(({ + data = { medications: { current: [], past: [] } }, + onChange, + onValidationChange +}) => { + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [showPastMeds, setShowPastMeds] = useState(false); + const [focusedField, setFocusedField] = useState(null); + + const FREQUENCIES = [ + 'Once daily', 'Twice daily', 'Three times daily', + 'Every 12 hours', 'Every 8 hours', 'As needed' + ]; + + const COMMON_SIDE_EFFECTS = [ + 'Nausea', 'Drowsiness', 'Dizziness', 'Headache', + 'Fatigue', 'Dry mouth', 'Insomnia', 'Appetite changes' + ]; + + const getDefaultMedication = (type) => ({ + name: '', + dosage: '', + frequency: '', + startDate: '', + prescribedBy: '', + purpose: '', + sideEffects: [], + ...(type === 'past' ? { endDate: '', reason: '' } : {}) + }); + + const validateMedication = (med, index, type) => { + const newErrors = {}; + + if (!med.name?.trim()) { + newErrors[`${type}${index}_name`] = 'Medication name is required'; + } + if (!med.startDate) { + newErrors[`${type}${index}_startDate`] = 'Start date is required'; + } + + if (type === 'med') { + if (!med.dosage?.trim()) { + newErrors[`${type}${index}_dosage`] = 'Dosage is required'; + } + if (!med.frequency) { + newErrors[`${type}${index}_frequency`] = 'Frequency is required'; + } + } + + if (type === 'pastMed') { + if (!med.endDate) { + newErrors[`${type}${index}_endDate`] = 'End date is required'; + } else if (med.startDate && med.endDate && new Date(med.endDate) < new Date(med.startDate)) { + newErrors[`${type}${index}_endDate`] = 'End date must be after start date'; + } + } + + return newErrors; + }; + + const validateAllMedications = (medications = {}) => { + let allErrors = {}; + + medications.current?.forEach((med, index) => { + const errors = validateMedication(med, index, 'med'); + allErrors = { ...allErrors, ...errors }; + }); + + medications.past?.forEach((med, index) => { + const errors = validateMedication(med, index, 'pastMed'); + allErrors = { ...allErrors, ...errors }; + }); + + setErrors(allErrors); + onValidationChange(Object.keys(allErrors).length === 0); + return allErrors; + }; + + const handleAddMedication = (type = 'current') => { + const medications = { + current: [], + past: [], + ...(data.medications || {}), + }; + + medications[type] = [...(medications[type] || []), getDefaultMedication(type)]; + onChange({ ...data, medications }); + }; + + const handleRemoveMedication = (index, type = 'current') => { + const medications = { + ...data.medications, + [type]: [...(data.medications[type] || [])] + }; + medications[type].splice(index, 1); + onChange({ ...data, medications }); + validateAllMedications(medications); + }; + + const handleMedicationChange = (index, field, value, type = 'current') => { + const medications = { + ...data.medications, + [type]: [...(data.medications[type] || [])] + }; + + if (!medications[type][index]) { + medications[type][index] = getDefaultMedication(type); + } + + medications[type][index] = { + ...medications[type][index], + [field]: value + }; + + onChange({ ...data, medications }); + + setTouched(prev => ({ + ...prev, + [`${type === 'past' ? 'pastMed' : 'med'}${index}_${field}`]: true + })); + + validateAllMedications(medications); + }; + + const handleSideEffectToggle = (index, effect) => { + const medications = { + ...data.medications, + current: [...(data.medications.current || [])] + }; + + const currentEffects = medications.current[index].sideEffects || []; + medications.current[index] = { + ...medications.current[index], + sideEffects: currentEffects.includes(effect) + ? currentEffects.filter(e => e !== effect) + : [...currentEffects, effect] + }; + + onChange({ ...data, medications }); + }; + + const handleFocus = (fieldId) => { + setFocusedField(fieldId); + }; + + const handleBlur = (fieldId) => { + setFocusedField(null); + setTouched(prev => ({ + ...prev, + [fieldId]: true + })); + }; + + useEffect(() => { + validateAllMedications(data.medications); + }, []); + + return ( + +
+

Medications

+ setShowPastMeds(!showPastMeds)} + className="flex items-center gap-2 text-sm text-teal-600 hover:text-teal-700 + transition-colors duration-300 group focus:outline-none" + > + + {showPastMeds ? 'Show Current Medications' : 'Show Past Medications'} + +
+ + + {showPastMeds ? ( + + {(!data.medications?.past || data.medications.past.length === 0) ? ( + handleAddMedication('past')} + className="w-full p-6 border-2 border-dashed border-gray-200 rounded-2xl + text-gray-500 hover:text-teal-500 hover:border-teal-500 + transition-all duration-300 flex items-center justify-center gap-2 + group hover:bg-teal-50/50 focus:outline-none" + > + + Add Past Medication + + ) : ( +
+ {data.medications.past.map((medication, index) => ( + + ))} + + handleAddMedication('past')} + className="w-full p-4 bg-gray-50 rounded-xl text-gray-500 + hover:bg-teal-50 hover:text-teal-500 transition-all duration-300 + flex items-center justify-center gap-2 group focus:outline-none" + > + + Add Another Past Medication + +
+ )} +
+ ) : ( + + {(!data.medications?.current || data.medications.current.length === 0) ? ( + handleAddMedication('current')} + className="w-full p-6 border-2 border-dashed border-gray-200 rounded-2xl + text-gray-500 hover:text-teal-500 hover:border-teal-500 + transition-all duration-300 flex items-center justify-center gap-2 + group hover:bg-teal-50/50 focus:outline-none" + > + + Add Current Medication + + ) : ( +
+ {data.medications.current.map((medication, index) => ( + + ))} + + handleAddMedication('current')} + className="w-full p-4 bg-gray-50 rounded-xl text-gray-500 + hover:bg-teal-50 hover:text-teal-500 transition-all duration-300 + flex items-center justify-center gap-2 group focus:outline-none" + > + + Add Another Current Medication + +
+ )} +
+ )} +
+ + + + Keep your medication information up to date for better healthcare + + + +
+ ); +}); + +export default MedicationsStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/NewPasswordStep.jsx b/healthhub-frontend/src/components/NewPasswordStep.jsx new file mode 100644 index 0000000..c15bed9 --- /dev/null +++ b/healthhub-frontend/src/components/NewPasswordStep.jsx @@ -0,0 +1,404 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Lock, Eye, EyeOff, Check, Shield, Heart, + AlertCircle, LockKeyhole, Fingerprint, Key +} from 'lucide-react'; + +const HeartbeatLine = ({ top, opacity = 1 }) => { + return ( +
+ + + +
+ ); +}; + +const PulseCircle = ({ size = "lg", delay = 0 }) => { + const sizes = { + sm: "h-8 w-8", + md: "h-12 w-12", + lg: "h-16 w-16" + }; + + return ( +
+
+
+
+ ); +}; + +const PasswordStrengthIndicator = ({ password, confirmPassword }) => { + const requirements = [ + { + text: "8+ characters long", + met: password.length >= 8, + icon: LockKeyhole + }, + { + text: "Passwords match", + met: password === confirmPassword && password !== '', + icon: Check + } + ]; + + return ( + +
+ + Password Requirements +
+
+ {requirements.map((req, index) => ( + +
+ + {req.text} + + {req.met && ( + + + + )} + + ))} +
+
+ ); +}; + +export const NewPasswordStep = ({ onSubmit }) => { + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [error, setError] = useState(''); + const [activeField, setActiveField] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + const validatePassword = () => { + if (newPassword.length < 8) { + setError('Password must be at least 8 characters'); + return false; + } + if (newPassword !== confirmPassword) { + setError('Passwords do not match'); + return false; + } + return true; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validatePassword()) { + return; + } + + try { + setIsLoading(true); + setError(''); + + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + newPassword, + type: 'password_reset', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Failed to reset password'); + } + + const data = await response.json(); + + if (response.json()) { + setIsSuccess(true); + setTimeout(async () => { + await onSubmit(newPassword); + }, 1500); + } else { + throw new Error('Password reset incomplete'); + } + + } catch (error) { + setError(error.message || 'Failed to reset password. Please try again.'); + setIsSuccess(false); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+ +
+
+
+ +
+

+ {isSuccess ? 'Password Reset Complete' : 'Reset Password'} +

+
+
+ +
+
+
+ +
+ + { + setNewPassword(e.target.value); + setError(''); + }} + onFocus={() => setActiveField('password')} + onBlur={() => setActiveField(null)} + className="w-full pl-12 pr-12 py-4 border-2 border-gray-200 rounded-xl + focus:ring-4 focus:ring-teal-500/20 focus:border-teal-500 + transition-all duration-300 group-hover:border-gray-300" + placeholder="Enter new password" + disabled={isLoading || isSuccess} + /> + +
+
+ +
+ +
+ + { + setConfirmPassword(e.target.value); + setError(''); + }} + onFocus={() => setActiveField('confirm')} + onBlur={() => setActiveField(null)} + className="w-full pl-12 pr-12 py-4 border-2 border-gray-200 rounded-xl + focus:ring-4 focus:ring-teal-500/20 focus:border-teal-500 + transition-all duration-300 group-hover:border-gray-300" + placeholder="Confirm new password" + disabled={isLoading || isSuccess} + /> + +
+
+ + + {error && ( + + + {error} + + )} + + + + + + {isLoading ? ( + <> +
+ +
+ Resetting... + + ) : isSuccess ? ( + <> + + Password Reset Complete + + ) : ( + <> + + Reset Password + + )} +
+ +
+
+
+
+ +
+
+ + + + + + + + + +
+
+
+ +
+
+

+ HealthHub +

+
+ +
+
+

+ Create Your New Password +

+

+ Choose a strong password to secure your account +

+
+ +
+ {[ + ].map((feature, index) => ( +
+
+ +
+
+ + {feature.text} + +
+ ))} +
+
+ +
+
+
+ +
+
+

Password Protected

+

Your data is secure with us

+
+
+
+
+
+
+
+ ); +}; + +export default NewPasswordStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/OTPStep.jsx b/healthhub-frontend/src/components/OTPStep.jsx new file mode 100644 index 0000000..9e759e4 --- /dev/null +++ b/healthhub-frontend/src/components/OTPStep.jsx @@ -0,0 +1,487 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Shield, ArrowRight, Check, LockKeyhole, RefreshCw, + Sparkles, Heart, AlertCircle +} from 'lucide-react'; + +const HeartbeatLine = ({ top, opacity = 1 }) => { + return ( +
+ + + +
+ ); +}; + +const TimerCircle = ({ seconds, maxSeconds }) => { + const radius = 20; + const circumference = 2 * Math.PI * radius; + const progress = (seconds / maxSeconds) * circumference; + + return ( +
+ + + + +
+ {seconds} +
+
+ ); +}; + +const OTPInput = ({ + value, + onChange, + index, + isFocused, + onFocus, + onKeyDown, + isComplete, + disabled +}) => { + const inputRef = useRef(null); + const [isAnimating, setIsAnimating] = useState(false); + + useEffect(() => { + if (isFocused && inputRef.current) { + inputRef.current.focus(); + } + }, [isFocused]); + + useEffect(() => { + if (value) { + setIsAnimating(true); + const timer = setTimeout(() => setIsAnimating(false), 300); + return () => clearTimeout(timer); + } + }, [value]); + + return ( +
+ + onChange(index, e.target.value)} + onKeyDown={(e) => onKeyDown(index, e)} + onFocus={() => onFocus(index)} + disabled={disabled} + className={` + w-14 h-16 text-center text-2xl font-bold + border-2 rounded-xl outline-none + transition-all duration-300 + ${disabled ? 'bg-gray-100 cursor-not-allowed' : ''} + ${isFocused + ? 'border-teal-500 bg-teal-50/50 ring-4 ring-teal-500/20' + : 'border-gray-200 hover:border-gray-300' + } + ${value + ? 'bg-gradient-to-br from-teal-50 to-blue-50 text-teal-600' + : 'bg-white text-gray-700' + } + ${isAnimating ? 'animate-bounce-soft' : ''} + ${isComplete && value ? 'border-green-500' : ''} + `} + /> + + + + + {isAnimating && value && ( + + )} +
+ ); +}; + +export const OTPStep = ({ onSubmit, onBack , email, onVerificationComplete }) => { + const [otp, setOtp] = useState(['', '', '', '', '', '']); + const [timer, setTimer] = useState(30); + const [canResend, setCanResend] = useState(false); + const [error, setError] = useState(''); + const [focusedInput, setFocusedInput] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [verificationComplete, setVerificationComplete] = useState(false); + + useEffect(() => { + setFocusedInput(0); + }, []); + + useEffect(() => { + let interval; + if (timer > 0 && !canResend) { + interval = setInterval(() => { + setTimer((prev) => prev <= 1 ? (setCanResend(true), 0) : prev - 1); + }, 1000); + } + return () => clearInterval(interval); + }, [timer]); + + const handleOTPChange = (index, value) => { + if (value.length > 1 || !/^\d*$/.test(value)) return; + + const newOTP = [...otp]; + newOTP[index] = value; + setOtp(newOTP); + setError(''); + + if (value && index < 5) { + setFocusedInput(index + 1); + } + }; + + const handleKeyDown = (index, e) => { + if (e.key === 'Backspace' && !otp[index] && index > 0) { + setFocusedInput(index - 1); + e.preventDefault(); + } else if (e.key === 'ArrowLeft' && index > 0) { + setFocusedInput(index - 1); + e.preventDefault(); + } else if (e.key === 'ArrowRight' && index < 5) { + setFocusedInput(index + 1); + e.preventDefault(); + } + }; + + const makeAPICall = async (endpoint, data) => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + throw new Error('Request failed'); + } + + return await response.json(); + } catch (error) { + throw new Error('An error occurred. Please try again.'); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!otp.every(digit => digit !== '')) { + setError('Please enter the complete OTP'); + return; + } + + setIsLoading(true); + setError(''); + + try { + await makeAPICall('/verify', { + email, + otp: otp.join(''), + type: 'otp_verification', + timestamp: new Date().toISOString() + }); + + setVerificationComplete(true); + onSubmit(otp); + setTimeout(() => { + onVerificationComplete?.(); + }, 1000); + } catch (error) { + setError(error.message); + } finally { + setIsLoading(false); + } + }; + + const handleResend = async () => { + setIsLoading(true); + setError(''); + + try { + await makeAPICall('/resend', { + email, + type: 'otp_resend', + timestamp: new Date().toISOString() + }); + + setCanResend(false); + setTimer(30); + setOtp(['', '', '', '', '', '']); + setFocusedInput(0); + } catch (error) { + setError(error.message); + } finally { + setIsLoading(false); + } + }; + + const isComplete = otp.every(digit => digit !== ''); + + return ( +
+
+
+
+ + + + + +
+
+
+ +
+
+

HealthHub

+
+ +
+
+

OTP Verification

+

+ We've sent a verification code to + + {email} + +

+
+
+
+ +
+
+
+ +
+
+

Enhanced Security

+

Multi-factor authentication enabled

+
+
+
+
+ +
+
+ +
+
+
+ +
+

Verify Your Email

+
+
+ +
+
+
+ {otp.map((digit, index) => ( + + ))} +
+ + {error && ( + + + {error} + + )} + +
+ {!canResend ? ( +
+ + until resend +
+ ) : ( + + + Resend Code + + )} +
+ + + {isLoading ? ( +
+ ) : verificationComplete ? ( +
+ + Verified Successfully! +
+ ) : ( + <> + Verify Email + + + )} + + +
+ +
+
+
+ {['OTP Verification', 'End-to-End Encrypted', 'Secure Channel'].map((text, i) => ( +
+ + {text} +
+ ))} +
+ +
+ + Secure verification session +
+
+
+ + + +

+ Didn't receive the code? Check your spam folder or{' '} + +

+
+
+
+
+ + +
+ ); +}; + +export default OTPStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/authentication.jsx b/healthhub-frontend/src/components/authentication.jsx index 5bb650b..c69e50f 100644 --- a/healthhub-frontend/src/components/authentication.jsx +++ b/healthhub-frontend/src/components/authentication.jsx @@ -1,19 +1,20 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { Heart, LogIn, UserPlus, Eye, EyeOff, Mail, Lock, User, ArrowRight, Check, Shield, Activity, Calendar, X, Sparkles } from 'lucide-react' +import { useNavigate } from 'react-router-dom'; const HeartbeatLine = ({ top, opacity = 1 }) => { return ( -
{ />
- ); -}; + ) +} + const PulseCircle = ({ size = "lg", color = "white", delay = 0 }) => { const sizes = { sm: "h-8 w-8", @@ -38,7 +40,6 @@ const PulseCircle = ({ size = "lg", color = "white", delay = 0 }) => { lg: "h-16 w-16" } - return (
{ ) } -const AuthPage = () => { +const AuthPage = ({ onComplete }) => { const [isLogin, setIsLogin] = useState(true) const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [activeField, setActiveField] = useState(null) const [isLoading, setIsLoading] = useState(false) const [formComplete, setFormComplete] = useState(false) + const navigate = useNavigate(); const [formData, setFormData] = useState({ email: '', password: '', - confirmPassword: '' + confirmPassword: '', + fullName: '' }) const [passwordsMatch, setPasswordsMatch] = useState(true) const [passwordError, setPasswordError] = useState('') @@ -95,6 +98,11 @@ const AuthPage = () => { duration: 8 + i * 2 }))) + const validateEmail = (email) => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); + }; + const handleInputChange = (e) => { const { name, value } = e.target setFormData(prev => ({ @@ -110,6 +118,11 @@ const AuthPage = () => { ) } } + + // Clear any previous errors + if (passwordError) { + setPasswordError(''); + } } const validatePasswords = (password, confirmPassword) => { @@ -127,9 +140,14 @@ const AuthPage = () => { const isFormValid = () => { if (isLogin) { - return formData.email && formData.password + return formData.email && formData.password && validateEmail(formData.email) } else { - return formData.email && formData.password && formData.confirmPassword && passwordsMatch + return formData.email && + formData.password && + formData.confirmPassword && + passwordsMatch && + formData.fullName && + validateEmail(formData.email) } } @@ -138,19 +156,40 @@ const AuthPage = () => { if (!isFormValid()) return setIsLoading(true) - await new Promise(resolve => setTimeout(resolve, 1500)) - setFormComplete(true) - setTimeout(() => { - setIsLoading(false) - setFormComplete(false) - }, 1000) + + if (isLogin) { + // Handle login logic + await new Promise(resolve => setTimeout(resolve, 1500)) + setFormComplete(true) + setTimeout(() => { + setIsLoading(false) + setFormComplete(false) + // Handle login success + }, 1000) + } else { + try { + // Handle signup with OTP + await new Promise(resolve => setTimeout(resolve, 1500)) + setFormComplete(true) + setTimeout(() => { + setIsLoading(false) + setFormComplete(false) + // Trigger OTP verification + onComplete(formData.email) + }, 1000) + } catch (error) { + setPasswordError('Something went wrong. Please try again.') + setIsLoading(false) + } + } } + // Rest of your component's JSX remains the same until the form part return (
+ {/* Left side with animations */}
-
@@ -158,14 +197,12 @@ const AuthPage = () => {
-
-
@@ -176,8 +213,9 @@ const AuthPage = () => { -
+
+ {/* Content section */}
@@ -265,15 +303,25 @@ const AuthPage = () => {
+ {/* Right side with form */}
-

+

{isLogin ? 'Welcome Back' : 'Create Account'}

@@ -304,10 +352,7 @@ const AuthPage = () => {
{!isLogin && ( -
+
@@ -315,19 +360,18 @@ const AuthPage = () => { setActiveField('name')} - onBlur={() => setActiveField(null)} - /> + onBlur={() => setActiveField(null)} />
)} -
+
@@ -346,13 +390,21 @@ const AuthPage = () => {
-
- +
+
+ + {isLogin && ( + + )} +
{ onClick={() => setShowPassword(!showPassword)} className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors" > - {showPassword ? - : - - } + {showPassword ? : }
{!isLogin && ( -
+
@@ -393,8 +439,8 @@ const AuthPage = () => { name="confirmPassword" value={formData.confirmPassword} onChange={handleInputChange} - className={`w-full pl-12 pr-12 py-4 border-2 ${passwordError ? 'border-red-300' : 'border-gray-200' - } rounded-2xl focus:ring-4 focus:ring-teal-500/20 focus:border-teal-500 transition-all duration-300`} + className={`w-full pl-12 pr-12 py-4 border-2 ${passwordError ? 'border-red-300' : 'border-gray-200'} + rounded-2xl focus:ring-4 focus:ring-teal-500/20 focus:border-teal-500 transition-all duration-300`} placeholder="Confirm your password" onFocus={() => setActiveField('confirmPassword')} onBlur={() => setActiveField(null)} @@ -404,10 +450,7 @@ const AuthPage = () => { onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors" > - {showConfirmPassword ? - : - - } + {showConfirmPassword ? : }
{passwordError && ( @@ -423,9 +466,9 @@ const AuthPage = () => { type="submit" disabled={!isFormValid() || isLoading} className={`w-full bg-gradient-to-r from-teal-500 to-blue-600 text-white py-4 rounded-2xl - transform transition-all duration-300 hover:scale-105 hover:shadow-xl - flex items-center justify-center gap-3 relative overflow-hidden text-lg font-semibold - ${!isFormValid() ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-90'}`} + transform transition-all duration-300 hover:scale-105 hover:shadow-xl + flex items-center justify-center gap-3 relative overflow-hidden text-lg font-semibold + ${!isFormValid() ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-90'}`} > {isLoading ? (
@@ -449,11 +492,11 @@ const AuthPage = () => { Password must:
  • -
    = 8 ? 'bg-green-500' : 'bg-gray-300'}`}>
    +
    = 8 ? 'bg-green-500' : 'bg-gray-300'}`} /> Be at least 8 characters long
  • -
    +
    Passwords must match
diff --git a/healthhub-frontend/src/components/basic_information.jsx b/healthhub-frontend/src/components/basic_information.jsx new file mode 100644 index 0000000..fc12b3b --- /dev/null +++ b/healthhub-frontend/src/components/basic_information.jsx @@ -0,0 +1,327 @@ +import React, { useState, useEffect, useRef, memo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { User, Calendar, Activity, Ruler, Weight, AlertCircle } from 'lucide-react'; + +const InputField = memo(({ + icon: Icon, + label, + name, + type = "text", + value = '', + onChange, + onFocus, + onBlur, + error, + touched, + isFocused, + placeholder, + ...props + }) => ( +
+ +
+ onChange(name, e.target.value)} + onFocus={() => onFocus(name)} + onBlur={() => onBlur(name)} + className={` + w-full px-4 py-3 pl-12 rounded-xl border-2 + transition-colors duration-300 + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + placeholder={placeholder} + {...props} + /> + + +
+ {error && touched && ( + + + {error} + + )} +
+
+
+ )); + + const SelectButton = memo(({ options, name, value = '', onChange, error, touched }) => ( +
+
+ {options.map((option) => ( + onChange(name, option)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + className={` + px-4 py-2 rounded-xl border-2 transition-colors duration-300 + ${value === option + ? 'border-teal-500 bg-teal-50 text-teal-600' + : 'border-gray-200 hover:border-gray-300' + } + ${error && touched ? 'border-red-300 bg-red-50 text-red-600' : ''} + `} + > + {option} + + ))} +
+ +
+ {error && touched && ( + + + {error} + + )} +
+
+ )); + +const BasicInfoStep = ({ + data = { + fullName: '', + dateOfBirth: '', + gender: '', + bloodType: '', + height: '', + weight: '' + }, + onChange = () => {}, + onValidationChange = () => {} +}) => { + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [focusedField, setFocusedField] = useState(null); + const [validationMessage, setValidationMessage] = useState(null); + + const validateField = (name, value) => { + switch (name) { + case 'fullName': + if (!value?.trim()) return 'Full name is required'; + if (value.length < 2) return 'Name is too short'; + if (!/^[a-zA-Z\s'-]+$/.test(value)) return 'Please enter a valid name'; + return null; + case 'dateOfBirth': + if (!value) return 'Date of birth is required'; + const date = new Date(value); + const age = new Date().getFullYear() - date.getFullYear(); + if (age < 0 || age > 120) return 'Invalid date'; + return null; + case 'height': + if (!value) return 'Height is required'; + const height = parseFloat(value); + if (isNaN(height) || height < 30 || height > 250) return 'Please enter a valid height'; + return null; + case 'weight': + if (!value) return 'Weight is required'; + const weight = parseFloat(value); + if (isNaN(weight) || weight < 20 || weight > 300) return 'Please enter a valid weight'; + return null; + case 'gender': + return !value ? 'Please select a gender' : null; + case 'bloodType': + return !value ? 'Please select blood type' : null; + default: + return null; + } + }; + + const handleChange = (name, value) => { + const newData = { ...data, [name]: value }; + const error = validateField(name, value); + + setErrors(prev => ({ + ...prev, + [name]: error + })); + + setTouched(prev => ({ + ...prev, + [name]: true + })); + + onChange(newData); + validateForm(newData); + }; + + const handleFocus = (name) => { + setFocusedField(name); + }; + + const handleBlur = (name) => { + setFocusedField(null); + const error = validateField(name, data[name]); + setErrors(prev => ({ + ...prev, + [name]: error + })); + }; + + const validateForm = (formData) => { + const newErrors = {}; + let hasErrors = false; + + Object.keys(formData).forEach(field => { + const error = validateField(field, formData[field]); + if (error) { + newErrors[field] = error; + hasErrors = true; + } + }); + + setErrors(newErrors); + setValidationMessage(hasErrors ? 'Please fill in all required fields marked with an asterisk (*)' : null); + onValidationChange?.(!hasErrors); + }; + + useEffect(() => { + validateForm(data); + }, []); + + return ( +
+
+ + + + +
+ + +
+ +
+ + + +
+ +
+ + +
+
+ + + {validationMessage && ( + + + {validationMessage} + + )} + +
+ ); +}; + +export default BasicInfoStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/contact_detail.jsx b/healthhub-frontend/src/components/contact_detail.jsx new file mode 100644 index 0000000..6d824fe --- /dev/null +++ b/healthhub-frontend/src/components/contact_detail.jsx @@ -0,0 +1,353 @@ +import React, { useState, useEffect, memo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Mail, Phone, MapPin, AlertCircle, Home, Building, Globe } from 'lucide-react'; + +const InputField = memo(({ + icon: Icon, + label, + name, + type = "text", + value = '', + onChange, + onFocus, + onBlur, + error, + touched, + isFocused, + placeholder, + required = false, + ...props +}) => ( +
+ +
+ onChange(name, e.target.value)} + onFocus={() => onFocus(name)} + onBlur={() => onBlur(name)} + className={` + w-full px-4 py-3 pl-12 rounded-xl border-2 + transition-colors duration-300 + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + disabled:bg-gray-50 disabled:text-gray-500 + `} + placeholder={placeholder} + {...props} + /> + + +
+ + {error && touched && ( + + + {error} + + )} + +
+
+
+)); + +const ContactDetailsStep = ({ + data = { + email: '', + phone: '', + address: { + street: '', + city: '', + state: '', + postalCode: '', + country: '' + } + }, + onChange = () => {}, + onValidationChange = () => {} +}) => { + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [focusedField, setFocusedField] = useState(null); + const [validationMessage, setValidationMessage] = useState(null); + + const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const PHONE_PATTERN = /^\+?[\d\s-]{10,}$/; + const POSTAL_CODE_PATTERN = /^[A-Z\d]{3,10}$/i; + + const validateField = (name, value) => { + if (!value?.trim()) { + if (name === 'email' || + name === 'phone' || + name === 'address.street' || + name === 'address.city' || + name === 'address.country') { + return `${name.split('.').pop().charAt(0).toUpperCase() + name.split('.').pop().slice(1)} is required`; + } + } + + switch (name) { + case 'email': + if (!EMAIL_PATTERN.test(value)) { + return 'Please enter a valid email address'; + } + break; + case 'phone': + if (!PHONE_PATTERN.test(value)) { + return 'Please enter a valid phone number'; + } + break; + case 'address.postalCode': + if (value && !POSTAL_CODE_PATTERN.test(value)) { + return 'Please enter a valid postal code'; + } + break; + default: + if (value && value.length > 100) { + return 'Input is too long'; + } + break; + } + return null; + }; + + const handleChange = (name, value) => { + const newData = { ...data }; + + if (name.startsWith('address.')) { + const addressField = name.split('.')[1]; + newData.address = { + ...newData.address, + [addressField]: value + }; + } else { + newData[name] = value; + } + + const error = validateField(name, value); + setErrors(prev => ({ + ...prev, + [name]: error + })); + + setTouched(prev => ({ + ...prev, + [name]: true + })); + + onChange(newData); + validateForm(newData); + }; + + const handleFocus = (name) => { + setFocusedField(name); + }; + + const handleBlur = (name) => { + setFocusedField(null); + const value = name.includes('address.') + ? data.address[name.split('.')[1]] + : data[name]; + const error = validateField(name, value); + setErrors(prev => ({ + ...prev, + [name]: error + })); + }; + + const validateForm = (formData) => { + const newErrors = {}; + let hasErrors = false; + + ['email', 'phone'].forEach(field => { + const error = validateField(field, formData[field]); + if (error) { + newErrors[field] = error; + hasErrors = true; + } + }); + + ['street', 'city', 'state', 'postalCode', 'country'].forEach(field => { + const error = validateField(`address.${field}`, formData.address?.[field]); + if (error) { + newErrors[`address.${field}`] = error; + hasErrors = true; + } + }); + + setErrors(newErrors); + const validationMsg = hasErrors ? 'Please complete all required fields marked with an asterisk (*)' : null; + setValidationMessage(validationMsg); + onValidationChange?.(!hasErrors); + }; + + useEffect(() => { + validateForm(data); + }, []); + + return ( +
+ +
+ + + +
+ +
+
+ +

Address Information

+
+ + + +
+ + + +
+ +
+ + + +
+
+
+ + + {validationMessage && ( + + + {validationMessage} + + )} + +
+ ); +}; + +export default ContactDetailsStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/emergency_contact.jsx b/healthhub-frontend/src/components/emergency_contact.jsx new file mode 100644 index 0000000..97d4b65 --- /dev/null +++ b/healthhub-frontend/src/components/emergency_contact.jsx @@ -0,0 +1,314 @@ +import React, { useState, useEffect, memo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { User, Phone, Mail, UserPlus, AlertCircle, X, Heart, UserCheck } from 'lucide-react'; + +const InputField = memo(({ + icon: Icon, + label, + name, + type = "text", + value = '', + onChange, + onFocus, + onBlur, + error, + touched, + isFocused, + placeholder, + required = false, + contactIndex, + ...props +}) => ( +
+ +
+ onChange(contactIndex, name, e.target.value)} + onFocus={() => onFocus(`${contactIndex}_${name}`)} + onBlur={() => onBlur(`${contactIndex}_${name}`)} + className={` + w-full px-4 py-3 pl-12 rounded-xl border-2 + transition-colors duration-300 focus:outline-none + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + placeholder={placeholder} + {...props} + /> + + +
+ + {error && touched && ( + + + {error} + + )} + +
+
+
+)); + +const EmergencyContactCard = memo(({ + contact, + index, + onRemove, + onChange, + onFocus, + onBlur, + errors, + touched, + focusedField, + isFirst, +}) => ( + + {!isFirst && ( + onRemove(index)} + className="absolute -top-2 -right-2 bg-red-100 p-2 rounded-full text-red-500 + opacity-0 group-hover:opacity-100 transform scale-90 group-hover:scale-100 + transition-all duration-300 hover:bg-red-200" + > + + + )} + +
+ {isFirst ? ( +
+ +
+ ) : ( +
+ +
+ )} + + {isFirst ? 'Primary Emergency Contact' : `Additional Contact ${index}`} + +
+ +
+ + + +
+ +
+ + + +
+
+)); + +const EmergencyContactStep = ({ + data = { emergencyContacts: [] }, + onChange = () => {}, + onValidationChange = () => {} +}) => { + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [focusedField, setFocusedField] = useState(null); + + useEffect(() => { + if (!data.emergencyContacts?.length) { + onChange({ + ...data, + emergencyContacts: [{ + name: '', + relationship: '', + phone: '', + email: '' + }] + }); + } + }, []); + + const isPrimaryContactValid = (contact) => { + return contact?.name?.trim() && + contact?.relationship?.trim() && + contact?.phone?.trim(); + }; + + const handleChange = (contactIndex, field, value) => { + const newContacts = [...(data.emergencyContacts || [])]; + if (!newContacts[contactIndex]) { + newContacts[contactIndex] = { name: '', relationship: '', phone: '', email: '' }; + } + newContacts[contactIndex] = { ...newContacts[contactIndex], [field]: value }; + + onChange({ ...data, emergencyContacts: newContacts }); + + const isPrimaryValid = isPrimaryContactValid(newContacts[0]); + onValidationChange(isPrimaryValid); + + setTouched(prev => ({ + ...prev, + [`${contactIndex}_${field}`]: true + })); + }; + + const handleAddContact = () => { + const newContacts = [...(data.emergencyContacts || [])]; + newContacts.push({ + name: '', + relationship: '', + phone: '', + email: '' + }); + onChange({ ...data, emergencyContacts: newContacts }); + }; + + const handleRemoveContact = (index) => { + if (index === 0) return; + const newContacts = [...(data.emergencyContacts || [])].filter((_, i) => i !== index); + onChange({ ...data, emergencyContacts: newContacts }); + }; + + const handleFocus = (fieldId) => { + setFocusedField(fieldId); + }; + + const handleBlur = (fieldId) => { + setFocusedField(null); + setTouched(prev => ({ + ...prev, + [fieldId]: true + })); + }; + + useEffect(() => { + if (data.emergencyContacts?.length) { + const isPrimaryValid = isPrimaryContactValid(data.emergencyContacts[0]); + onValidationChange(isPrimaryValid); + } + }, [data.emergencyContacts]); + + return ( +
+ + {data.emergencyContacts?.map((contact, index) => ( + + ))} + + + {data.emergencyContacts?.length < 3 && ( + + + Add Another Emergency Contact + + )} +
+ ); +}; + +export default EmergencyContactStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/medication_details.jsx b/healthhub-frontend/src/components/medication_details.jsx new file mode 100644 index 0000000..8e5c123 --- /dev/null +++ b/healthhub-frontend/src/components/medication_details.jsx @@ -0,0 +1,459 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + User, ChevronRight, ChevronLeft, Activity, Heart, + Phone, MapPin, AlertCircle, Calendar, Clock, + Plus, Minus, Check, ArrowRight, Sparkles, X +} from 'lucide-react'; + +import BasicInfoStep from './basic_information'; +import ContactDetailsStep from './contact_detail'; +import EmergencyContactStep from './emergency_contact'; +import VitalStatsStep from './vital_stats'; +import AllergiesStep from './Allergies'; +import MedicationsStep from './Medications'; +import ReviewStep from './review'; + +export const FORM_STEPS = [ + { + id: 'basic-info', + title: 'Basic Information', + subtitle: 'Let\'s start with your basic details', + icon: User, + required: true, + component: BasicInfoStep, + validateStep: (data) => { + return Boolean( + data.fullName?.trim() && + data.dateOfBirth && + data.gender && + data.bloodType + ); + } + }, + { + id: 'contact', + title: 'Contact Details', + subtitle: 'How can we reach you?', + icon: Phone, + required: true, + component: ContactDetailsStep, + validateStep: (data) => { + return Boolean( + data.email?.trim() && + data.phone?.trim() && + data.address?.street?.trim() && + data.address?.city?.trim() && + data.address?.country?.trim() + ); + } + }, + { + id: 'emergency-contact', + title: 'Emergency Contact', + subtitle: 'Someone we can contact if needed', + icon: AlertCircle, + required: false, + component: EmergencyContactStep, + validateStep: (data) => { + const primaryContact = data.emergencyContacts?.[0]; + return Boolean( + primaryContact?.name?.trim() && + primaryContact?.relationship?.trim() && + primaryContact?.phone?.trim() + ); + } + }, + { + id: 'vital-stats', + title: 'Vital Statistics', + subtitle: 'Your basic health metrics', + icon: Activity, + required: false, + component: VitalStatsStep + }, + { + id: 'allergies', + title: 'Allergies', + subtitle: 'Any allergies we should know about?', + icon: AlertCircle, + required: false, + component: AllergiesStep + }, + { + id: 'current-medications', + title: 'Current Medications', + subtitle: 'Medications you\'re currently taking', + icon: Heart, + required: false, + component: MedicationsStep + }, + { + id: 'review', + title: 'Review Profile', + subtitle: 'Review and confirm your information', + icon: Check, + required: true, + component: ReviewStep + } +]; + +const ProgressIndicator = ({ currentStep, totalSteps }) => { + return ( + + + + ); +}; + +const StepIndicator = ({ currentStep, step, index, completedSteps, skippedSteps }) => { + const isActive = currentStep === index; + const isPassed = currentStep > index; + const isSkipped = skippedSteps.has(index); + const isCompleted = completedSteps.has(index); + const StepIcon = step.icon; + + return ( + + + {(isCompleted || (isPassed && !isSkipped)) ? ( + + ) : ( + + )} + + {step.title} + + ); +}; + +const HealthProfileForm = ({ initialData, onComplete }) => { + const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState(() => { + const savedData = localStorage.getItem('healthProfile'); + return savedData ? JSON.parse(savedData) : {}; + }); + const [showAlert, setShowAlert] = useState(false); + const [alertMessage, setAlertMessage] = useState(''); + const [skippedSteps, setSkippedSteps] = useState(new Set()); + const [completedSteps, setCompletedSteps] = useState(new Set()); + const [stepValidation, setStepValidation] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitSuccess, setSubmitSuccess] = useState(false); + const formRef = useRef(null); + + useEffect(() => { + localStorage.setItem('healthProfile', JSON.stringify(formData)); + }, [formData]); + + const validateStep = useCallback((stepIndex, data) => { + const step = FORM_STEPS[stepIndex]; + if (!step.required) return true; + if (step.validateStep) { + return step.validateStep(data); + } + return true; + }, []); + + const handleDataChange = useCallback((newData) => { + setFormData(prev => { + const updated = { ...prev, ...newData }; + const isValid = validateStep(currentStep, updated); + setStepValidation(prev => ({ ...prev, [FORM_STEPS[currentStep].id]: isValid })); + return updated; + }); + }, [currentStep, validateStep]); + + const handleSubmit = async (formData) => { + setIsSubmitting(true); + setShowAlert(false); + + try { + await new Promise(resolve => setTimeout(resolve, 1500)); + + localStorage.setItem('healthProfile', JSON.stringify(formData)); + + setSubmitSuccess(true); + setAlertMessage('Profile created successfully!'); + setShowAlert(true); + + if (typeof onComplete === 'function') { + await onComplete(formData); + } + + setTimeout(() => { + setShowAlert(false); + }, 5000); + + return { + success: true, + message: 'Profile created successfully', + data: formData + }; + } catch (error) { + console.error('Error submitting form:', error); + setAlertMessage(error.message || 'Error submitting form. Please try again.'); + setShowAlert(true); + setSubmitSuccess(false); + } finally { + setIsSubmitting(false); + } + }; + const handleNext = () => { + if (currentStep === FORM_STEPS.length - 1) { + handleSubmit(); + return; + } + + const currentStepData = FORM_STEPS[currentStep]; + if (currentStepData.required && !validateStep(currentStep, formData)) { + setAlertMessage('Please complete all required fields'); + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 5000); + return; + } + + setCompletedSteps(prev => new Set([...prev, currentStep])); + setCurrentStep(prev => prev + 1); + setShowAlert(false); + }; + + const handleBack = () => { + if (currentStep > 0) { + setCurrentStep(prev => prev - 1); + setShowAlert(false); + } + }; + + const handleSkip = () => { + if (!FORM_STEPS[currentStep].required) { + setSkippedSteps(prev => new Set([...prev, currentStep])); + handleNext(); + } else { + setAlertMessage('This step is required and cannot be skipped'); + setShowAlert(true); + } + }; + + const CurrentStepIcon = FORM_STEPS[currentStep].icon; + const CurrentStepComponent = FORM_STEPS[currentStep].component; + + return ( + + + + + {showAlert && ( + +
+ {submitSuccess + ? + : + } + {alertMessage} +
+
+ )} +
+ +
+
+ {FORM_STEPS.map((step, index) => ( + + ))} +
+
+
+
+ + + +
+ +
+
+

+ {FORM_STEPS[currentStep].title} +

+

+ {FORM_STEPS[currentStep].subtitle} +

+
+
+ + + { + const stepIndex = FORM_STEPS.findIndex(step => step.id === sectionId); + if (stepIndex !== -1) { + setCurrentStep(stepIndex); + } + }} + onValidationChange={(isValid) => + setStepValidation(prev => ({ + ...prev, + [FORM_STEPS[currentStep].id]: isValid + })) + } + /> + + + + {currentStep !== FORM_STEPS.length - 1 && ( + <> + + + Back + + +
+ {!FORM_STEPS[currentStep].required && ( + + Skip for now + + )} + + + Continue + + +
+ + )} +
+
+
+
+ + +
+ ); +}; + +export default HealthProfileForm; \ No newline at end of file diff --git a/healthhub-frontend/src/components/otp_verification.jsx b/healthhub-frontend/src/components/otp_verification.jsx new file mode 100644 index 0000000..c96639d --- /dev/null +++ b/healthhub-frontend/src/components/otp_verification.jsx @@ -0,0 +1,461 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Shield, ArrowRight, Check, Mail, RefreshCw, Sparkles, LockKeyhole, Fingerprint, AlertCircle } from 'lucide-react'; + +const PulseCircle = ({ delay = 0, top, left, size = "lg" }) => { + const sizes = { + sm: "w-8 h-8", + md: "w-12 h-12", + lg: "w-16 h-16", + xl: "w-24 h-24" + }; + + return ( +
+
+
+
+ ); +}; + +const FloatingSparkle = ({ delay = 0, top, left }) => ( +
+ +
+); + +const HeartbeatLine = ({ top }) => ( +
+ + + +
+); + +const NumberInput = ({ value, onChange, onKeyDown, onFocus, onBlur, inputRef, isActive, index, isComplete }) => { + const [isFilled, setIsFilled] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + useEffect(() => { + if (value) { + setIsAnimating(true); + setIsFilled(true); + const timer = setTimeout(() => setIsAnimating(false), 300); + return () => clearTimeout(timer); + } else { + setIsFilled(false); + } + }, [value]); + + return ( +
+ {isActive && ( +
+ )} + + onChange(e)} + onKeyDown={onKeyDown} + onFocus={onFocus} + onBlur={onBlur} + className={` + relative w-14 h-16 sm:w-16 sm:h-20 text-center text-2xl sm:text-3xl font-bold + border-2 rounded-xl outline-none transition-all duration-300 + ${isActive + ? 'border-teal-500 bg-teal-50/50 ring-4 ring-teal-500/20 scale-110 z-10' + : 'border-gray-200 hover:border-teal-300' + } + ${isFilled + ? 'bg-gradient-to-br from-teal-50 to-blue-50 text-teal-600' + : 'bg-white text-gray-700' + } + ${isAnimating ? 'animate-bounce-soft' : ''} + ${isComplete && isFilled ? 'border-green-500' : ''} + transform hover:scale-105 focus:scale-110 + transition-all duration-300 ease-in-out + `} + /> + +
+ + {isFilled && isAnimating && ( + + )} +
+ ); +}; + +const OTPVerification = ({ email = "example@email.com", onVerificationComplete }) => { + const [otp, setOtp] = useState(['', '', '', '', '', '']); + const [isVerifying, setIsVerifying] = useState(false); + const [verificationComplete, setVerificationComplete] = useState(false); + const [timer, setTimer] = useState(30); + const [canResend, setCanResend] = useState(false); + const [activeInput, setActiveInput] = useState(null); + const [error, setError] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [isResending, setIsResending] = useState(false); + const [apiError, setApiError] = useState(null); + const inputRefs = useRef([]); + + useEffect(() => { + let interval; + if (timer > 0 && !canResend) { + interval = setInterval(() => { + setTimer((prev) => prev - 1); + }, 1000); + } else { + setCanResend(true); + } + return () => clearInterval(interval); + }, [timer]); + + const handleInputChange = (index, e) => { + const value = e.target.value; + if (value.length > 1) return; + if (!/^\d*$/.test(value)) return; + + setError(''); + const newOtp = [...otp]; + newOtp[index] = value; + setOtp(newOtp); + + if (value && index < 5) { + inputRefs.current[index + 1].focus(); + } + }; + + const handleKeyDown = (index, e) => { + if (e.key === 'Backspace' && !otp[index] && index > 0) { + inputRefs.current[index - 1].focus(); + } + }; + + const handleVerification = async () => { + try { + setIsVerifying(true); + setError(''); + + const otpCode = otp.join(''); + await verifyOTP(otpCode); + + setVerificationComplete(true); + setSuccessMessage('Verification successful!'); + + setTimeout(() => { + onVerificationComplete?.(); + }, 1000); + } catch (error) { + setError(error.message); + setIsVerifying(false); + } + }; + const verifyOTP = async (otpCode) => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + otp: otpCode, + type: 'verify_otp', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Invalid verification code'); + } + + const data = await response.json(); + if (response.json()) { + return true; + } + throw new Error('Verification failed'); + } catch (error) { + throw new Error('Failed to verify code. Please try again.'); + } + }; + const resendOTP = async () => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + type: 'resend_otp', + timestamp: new Date().toISOString() + }) + }); + + if (!response.ok) { + throw new Error('Failed to send new code'); + } + + await response.json(); + return true; + } catch (error) { + throw new Error('Failed to send new code. Please try again.'); + } + }; + const handleResendOTP = async () => { + try { + setCanResend(false); + await resendOTP(); + + setTimer(30); + setOtp(['', '', '', '', '', '']); + setActiveInput(0); + setSuccessMessage('New code sent successfully!'); + + setTimeout(() => setSuccessMessage(''), 3000); + } catch (error) { + setError(error.message); + setCanResend(true); + } + }; + + const validateOTP = (otpArray) => { + const otpString = otpArray.join(''); + if (otpString.length !== 6) { + throw new Error('Please enter all digits of the verification code'); + } + if (!/^\d+$/.test(otpString)) { + throw new Error('Verification code must contain only numbers'); + } + return otpString; + }; + const isComplete = otp.every(digit => digit !== ''); + + return ( +
+
+
+
+ + + + + + + + + + + + +
+
+
+ +
+
+

+ HealthHub +

+
+ +
+
+

+ Secure Authentication +

+

+ Your security is our top priority +

+
+ +
+ {['256-bit Encryption', 'HIPAA Compliant', 'Biometric Ready'].map((text, i) => ( +
+
+ {i === 0 ? : + i === 1 ? : + } +
+ {text} +
+ ))} +
+
+
+ +
+
+
+ +
+
+

Enhanced Security

+

Multi-factor authentication enabled

+
+
+
+
+ +
+
+
+
+
+
+ +
+

+ Verify Your Email +

+

+ We've sent a verification code to +

+

+ {email} +

+
+ + {error && ( +
+ + {error} +
+ )} + + {successMessage && ( +
+ + {successMessage} +
+ )} + +
+
+
+ {otp.map((digit, index) => ( + handleInputChange(index, e)} + onKeyDown={(e) => handleKeyDown(index, e)} + onFocus={() => setActiveInput(index)} + onBlur={() => setActiveInput(null)} + aria-label={`Digit ${index + 1} of verification code`} + role="textbox" + inputMode="numeric" + autoComplete="one-time-code" + inputRef={el => inputRefs.current[index] = el} + isActive={activeInput === index} + index={index} + isComplete={isComplete} + /> + ))} +
+ +
+ {!canResend ? ( +
+
+
{timer}
+
+ seconds until resend +
+ ) : ( + + )} +
+
+ + + +
+
+ + Secure Verification +
+
+
+
+
+
+
+
+
+ ); +}; + +export default OTPVerification; \ No newline at end of file diff --git a/healthhub-frontend/src/components/review.jsx b/healthhub-frontend/src/components/review.jsx new file mode 100644 index 0000000..328ad4a --- /dev/null +++ b/healthhub-frontend/src/components/review.jsx @@ -0,0 +1,431 @@ +import React, { useState } from 'react'; +import { + Check, AlertCircle, User, Phone, Heart, Activity, + Calendar, MapPin, Shield, Pill, Edit2, History +} from 'lucide-react'; + +const ReviewStep = ({ + data = { + fullName: '', + dateOfBirth: '', + gender: '', + bloodType: '', + height: '', + weight: '', + email: '', + phone: '', + address: {}, + emergencyContacts: [], + allergies: [], + medications: { + current: [], + past: [] + }, + vitalSigns: { + bloodPressure: [], + heartRate: [], + temperature: [], + oxygenSaturation: [] + } + }, + onEditSection, + onComplete, + isSubmitting = false +}) => { + const [error, setError] = useState(null); + + const formatDate = (dateString) => { + if (!dateString) return ''; + try { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } catch (e) { + return ''; + } + }; + + const SectionCard = ({ title, icon: Icon, children, sectionId, isComplete }) => ( +
+
+
+
+ +
+

{title}

+
+ +
+
{children}
+
+ ); + + const DataRow = ({ label, value, className = "" }) => ( +
+ {label} + {value || 'Not provided'} +
+ ); + + const isBasicInfoComplete = Boolean( + data.fullName && + data.dateOfBirth && + data.gender && + data.bloodType && + data.height && + data.weight + ); + + const isContactComplete = Boolean( + data.email && + data.phone && + data.address?.street && + data.address?.city && + data.address?.country + ); + + const isEmergencyContactComplete = Boolean( + data.emergencyContacts && + data.emergencyContacts.length > 0 && + data.emergencyContacts[0]?.name && + data.emergencyContacts[0]?.relationship && + data.emergencyContacts[0]?.phone + ); + + const formatVitalSign = (value, unit) => { + return value ? `${value} ${unit}` : 'Not recorded'; + }; + +const handleFormSubmit = async () => { + try { + if (typeof onComplete !== 'function') { + console.error('Form submission handler is not available'); + setError('Unable to submit form at this time. Please try again.'); + return; + } + + setError(null); + + const formattedData = { + personalInfo: { + fullName: data.fullName, + dateOfBirth: data.dateOfBirth, + gender: data.gender, + bloodType: data.bloodType, + physicalAttributes: { + height: parseFloat(data.height), + weight: parseFloat(data.weight) + } + }, + contactInfo: { + email: data.email, + phone: data.phone, + address: data.address + }, + emergencyContacts: data.emergencyContacts, + healthMetrics: { + vitalSigns: data.vitalSigns, + allergies: data.allergies || [], + medications: { + current: data.medications?.current || [], + past: data.medications?.past || [] + } + }, + metadata: { + lastUpdated: new Date().toISOString(), + version: "1.0", + submissionDate: new Date().toISOString() + } + }; + + const apiEndpoint = 'https://jsonplaceholder.typicode.com/posts'; + const response = await fetch(apiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formattedData) + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } + + const responseData = await response.json(); + console.log('Profile submission successful:', responseData); + + await onComplete(formattedData); + + } catch (error) { + console.error('Error submitting profile:', error); + setError(error.message || 'An error occurred while submitting your profile'); + } + }; + + return ( +
+ +
+ + + + + + +
+
+ + +
+ + + {data.address && ( + <> + + + + + + + )} +
+
+ + +
+ {data.emergencyContacts?.map((contact, index) => ( +
+
+ Contact {index + 1} +
+ + + + +
+ ))} + {!data.emergencyContacts?.length && ( +
+ No emergency contacts added +
+ )} +
+
+ + +
+
+ + + + +
+
+
+ + +
+ {data.allergies?.map((allergy, index) => ( +
+ + + + +
+ ))} + {!data.allergies?.length && ( +
+ No allergies reported +
+ )} +
+
+ + +
+ {data.medications?.current?.map((med, index) => ( +
+ + + + + + + +
+ ))} + {!data.medications?.current?.length && ( +
+ No current medications listed +
+ )} +
+
+ + +
+ {data.medications?.past?.map((med, index) => ( +
+ + + + + +
+ ))} + {!data.medications?.past?.length && ( +
+ No past medications listed +
+ )} +
+
+ + {!isBasicInfoComplete || !isContactComplete || !isEmergencyContactComplete ? ( +
+ + Please complete all required sections before submitting +
+ ) : ( +
+ + All required information has been provided +
+ )} + + {error && ( +
+ + {error} +
+ )} + + + + +
+ ); +}; + +export default ReviewStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/vital_stats.jsx b/healthhub-frontend/src/components/vital_stats.jsx new file mode 100644 index 0000000..69dfda5 --- /dev/null +++ b/healthhub-frontend/src/components/vital_stats.jsx @@ -0,0 +1,337 @@ +import React, { useState, useEffect, memo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Activity, Heart, Thermometer, Droplets, Clock, AlertCircle } from 'lucide-react'; + +const VitalInput = memo(({ + icon: Icon, + label, + type, + field, + value, + unit, + error, + min, + max, + placeholder, + onChange, + onFocus, + onBlur, + isFocused, + touched +}) => ( +
+ +
+ onChange(type, field, e.target.value)} + onFocus={() => onFocus(`${type}_${field}`)} + onBlur={() => onBlur(`${type}_${field}`)} + min={min} + max={max} + className={` + w-full px-4 py-3 pl-12 rounded-xl border-2 + transition-colors duration-300 focus:outline-none + ${error && touched + ? 'border-red-300 focus:border-red-500 focus:ring-4 focus:ring-red-500/20' + : 'border-gray-200 focus:border-teal-500 focus:ring-4 focus:ring-teal-500/20' + } + ${isFocused ? 'ring-4 ring-teal-500/20' : ''} + group-hover:border-gray-300 + `} + placeholder={placeholder} + /> + + {unit && ( + + {unit} + + )} + +
+ + {error && touched && ( + + + {error} + + )} + +
+
+
+)); + +const VitalStatsStep = ({ + data = { + vitalSigns: { + bloodPressure: [], + heartRate: [], + temperature: [], + oxygenSaturation: [] + } + }, + onChange, + onValidationChange +}) => { + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [focusedField, setFocusedField] = useState(null); + + const validateReading = (value, min, max, field) => { + if (!value) return null; + const num = parseFloat(value); + if (isNaN(num)) { + return `Please enter a valid number for ${field}`; + } + if (num < min || num > max) { + return `${field} should be between ${min} and ${max}`; + } + return null; + }; + + const handleVitalChange = (type, field, value) => { + const newData = { + ...data, + vitalSigns: { + ...(data.vitalSigns || {}), + [type]: [ + { + ...(data.vitalSigns?.[type]?.[0] || {}), + [field]: value, + timestamp: new Date().toISOString() + } + ] + } + }; + + const newErrors = { ...errors }; + let error = null; + + switch(type) { + case 'bloodPressure': + if (field === 'systolic') { + error = validateReading(value, 70, 190, 'Systolic pressure'); + } else if (field === 'diastolic') { + error = validateReading(value, 40, 100, 'Diastolic pressure'); + } + break; + case 'heartRate': + if (field === 'beatsPerMinute') { + error = validateReading(value, 40, 200, 'Heart rate'); + } + break; + case 'temperature': + if (field === 'value') { + error = validateReading(value, 35, 42, 'Temperature'); + } + break; + case 'oxygenSaturation': + if (field === 'percentage') { + error = validateReading(value, 80, 100, 'Oxygen saturation'); + } + break; + } + + if (error) { + newErrors[`${type}_${field}`] = error; + } else { + delete newErrors[`${type}_${field}`]; + } + + setErrors(newErrors); + onValidationChange(Object.keys(newErrors).length === 0); + onChange(newData); + + setTouched(prev => ({ + ...prev, + [`${type}_${field}`]: true + })); + }; + + const handleFocus = (fieldId) => { + setFocusedField(fieldId); + }; + + const handleBlur = (fieldId) => { + setFocusedField(null); + const [type, field] = fieldId.split('_'); + const value = data.vitalSigns?.[type]?.[0]?.[field]; + + setTouched(prev => ({ + ...prev, + [fieldId]: true + })); + + const error = validateReading( + value, + field === 'systolic' ? 70 : field === 'diastolic' ? 40 : + field === 'beatsPerMinute' ? 40 : field === 'value' ? 35 : 80, + field === 'systolic' ? 190 : field === 'diastolic' ? 100 : + field === 'beatsPerMinute' ? 200 : field === 'value' ? 42 : 100, + `${type} ${field}` + ); + + if (error) { + setErrors(prev => ({ + ...prev, + [fieldId]: error + })); + } + }; + + const currentVitals = { + bloodPressure: data.vitalSigns?.bloodPressure?.[0] || {}, + heartRate: data.vitalSigns?.heartRate?.[0] || {}, + temperature: data.vitalSigns?.temperature?.[0] || {}, + oxygenSaturation: data.vitalSigns?.oxygenSaturation?.[0] || {}, + }; + + useEffect(() => { + const hasErrors = Object.values(errors).some(error => error); + onValidationChange(!hasErrors); + }, []); + + return ( + + +
+ + +
+ + +
+ + + + + + + + + + All measurements are recorded with current timestamp + +
+ ); +}; + +export default memo(VitalStatsStep); \ No newline at end of file diff --git a/healthhub-frontend/src/main.jsx b/healthhub-frontend/src/main.jsx index d5591a7..a928eec 100644 --- a/healthhub-frontend/src/main.jsx +++ b/healthhub-frontend/src/main.jsx @@ -1,11 +1,15 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './styles/animations.css' -import AuthPage from './components/authentication.jsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import './styles/animations.css'; +import App from './App.jsx'; -createRoot(document.getElementById('root')).render( +const root = createRoot(document.getElementById('root')); + +root.render( - + + + , -) - +); \ No newline at end of file From 0ea96fa94aaa6d811e1b35aa176f674e0398d9f7 Mon Sep 17 00:00:00 2001 From: Rishi Garg Date: Wed, 1 Jan 2025 17:04:01 +0530 Subject: [PATCH 2/2] Some Fixes In Frontend Signed-off-by: Rishi Garg --- healthhub-frontend/index.html | 10 +- .../src/components/Dashboard.jsx | 990 ++++++++++++------ .../src/components/basic_information.jsx | 388 ++++--- .../src/components/contact_detail.jsx | 60 +- .../src/components/medication_details.jsx | 11 +- healthhub-frontend/src/components/review.jsx | 6 - healthhub-frontend/src/styles/animations.css | 2 +- healthhub-frontend/tailwind.config.js | 28 + 8 files changed, 909 insertions(+), 586 deletions(-) diff --git a/healthhub-frontend/index.html b/healthhub-frontend/index.html index e168034..621b40b 100644 --- a/healthhub-frontend/index.html +++ b/healthhub-frontend/index.html @@ -1,6 +1,14 @@ - + + diff --git a/healthhub-frontend/src/components/Dashboard.jsx b/healthhub-frontend/src/components/Dashboard.jsx index 89f6175..80f93b7 100644 --- a/healthhub-frontend/src/components/Dashboard.jsx +++ b/healthhub-frontend/src/components/Dashboard.jsx @@ -1,29 +1,185 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { User, Calendar, Hospital, Sun, Moon, Menu, X, MapPin, Clock, Bell, ChevronRight, Settings, - FileText, Heart, Activity, Plus + FileText, Heart, Activity, Plus, Search, Filter, + Loader, RefreshCcw, TrendingUp, ArrowUpRight } from 'lucide-react'; +const getColorClasses = (color) => { + const colorMap = { + blue: 'bg-blue-100 text-blue-500 dark:bg-blue-900/30 dark:text-blue-400', + teal: 'bg-teal-100 text-teal-500 dark:bg-teal-900/30 dark:text-teal-400', + purple: 'bg-purple-100 text-purple-500 dark:bg-purple-900/30 dark:text-purple-400', + pink: 'bg-pink-100 text-pink-500 dark:bg-pink-900/30 dark:text-pink-400', + }; + return colorMap[color]; + }; + + +// API fetch hook +const useHealthData = () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + try { + setLoading(true); + // Replace with your actual API endpoint + const response = await fetch('https://api.example.com/health-data'); + const jsonData = await response.json(); + setData(jsonData); + setError(null); + } catch (err) { + setError('Failed to fetch health data'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + return { data, loading, error, refetch: fetchData }; +}; + + + +// Animated stat card component +// Find and replace the existing StatCard component with this new one +const StatCard = ({ label, value, icon: Icon, color, animate = true }) => ( + +
+
+

+ {label} +

+ + {value} + +
+ + + +
+
+ ); + +// Search bar component +const SearchBar = () => ( +
+ + +
+); + +// Loading spinner component +const LoadingSpinner = () => ( + +); const Dashboard = () => { const [isDarkMode, setIsDarkMode] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [activeTab, setActiveTab] = useState('dashboard'); + const [showNotifications, setShowNotifications] = useState(false); + + // Use the health data hook + const { data: healthData, loading, error, refetch } = useHealthData(); const toggleDarkMode = () => { - setIsDarkMode(!isDarkMode); - document.documentElement.classList.toggle('dark'); + const root = document.documentElement; + const newMode = !isDarkMode; + + // Add animation class before toggling + root.classList.add('transitioning-theme'); + + // Toggle dark mode + if (newMode) { + root.classList.remove('light'); + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + root.classList.add('light'); + } + + // Update state + setIsDarkMode(newMode); + + // Save preference + localStorage.setItem('darkMode', newMode ? 'dark' : 'light'); + + // Remove animation class after transition + setTimeout(() => { + root.classList.remove('transitioning-theme'); + }, 300); }; - + + // Update the initial theme effect + useEffect(() => { + // Check saved preference or system preference + const savedMode = localStorage.getItem('darkMode'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + + // Set initial mode + const initialMode = savedMode === 'dark' || (!savedMode && prefersDark); + setIsDarkMode(initialMode); + + // Update HTML classes + const root = document.documentElement; + if (initialMode) { + root.classList.remove('light'); + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + root.classList.add('light'); + } + }, []); + const MenuItems = [ { id: 'dashboard', icon: Activity, label: 'Dashboard' }, { id: 'profile', icon: User, label: 'Edit Profile' }, - { id: 'appointments', icon: Calendar, label: 'Manage Appointments' }, - { id: 'hospitals', icon: Hospital, label: 'Nearby Hospitals' }, - { id: 'records', icon: FileText, label: 'Health Records' }, + { id: 'appointments', icon: Calendar, label: 'Appointments' }, + { id: 'hospitals', icon: Hospital, label: 'Hospitals' }, + { id: 'records', icon: FileText, label: 'Records' }, { id: 'settings', icon: Settings, label: 'Settings' } ]; + // Mock data - replace with API data in production const appointments = [ { id: 1, doctor: "Dr. Sarah Wilson", type: "General Checkup", date: "2024-01-15", time: "10:00 AM", status: "upcoming" }, { id: 2, doctor: "Dr. Michael Chen", type: "Dental", date: "2024-01-18", time: "2:30 PM", status: "upcoming" }, @@ -36,338 +192,552 @@ const Dashboard = () => { { id: 3, name: "Park View Hospital", distance: "3.8 km", availability: "Closed" } ]; + // Motion variants for animations + const sidebarVariants = { + open: { width: '16rem' }, + closed: { width: '5rem' } + }; + + const fadeInUpVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 } + }; + return ( -
- - -
-
-
-

- Welcome, John -

-
- - + + + {isSidebarOpen && ( + + {item.label} + + )} + + + ))} + + + + {/* Main Content Area */} + + {/* Top Bar */} +
+
+
+

+ Welcome, John +

+ +
+
+ + + {isDarkMode ? ( + + + + ) : ( + + + + )} + + + + setShowNotifications(!showNotifications)} + className="p-2 rounded-lg relative hover:bg-gray-100 + dark:hover:bg-gray-700 transition-colors duration-200" + > + + + + + {/* Notifications dropdown */} + + {showNotifications && ( + + {/* Add notifications content here */} + + )} + +
-
-
-
- {[ - { label: 'Upcoming Appointments', value: '3', icon: Calendar, color: 'blue' }, - { label: 'Nearby Hospitals', value: '8', icon: Hospital, color: 'teal' }, - { label: 'Recent Records', value: '12', icon: FileText, color: 'purple' }, - { label: 'Notifications', value: '5', icon: Bell, color: 'pink' } - ].map((stat, index) => ( -
+ {/* Stats Grid */} + + : '3'} + icon={Calendar} + color="blue" + /> + : '8'} + icon={Hospital} + color="teal" + /> + : '12'} + icon={FileText} + color="purple" + /> + : '5'} + icon={Bell} + color="pink" + /> + + + {/* Appointments and Hospitals Section */} +
+ {/* Appointments Card */} + -
-
-

- {stat.label} -

-

- {stat.value} -

+
+
+

+ Upcoming Appointments +

+ + + New +
-
- + +
+ + {appointments.map((appointment, index) => ( + +
+
+

+ {appointment.doctor} +

+

+ {appointment.type} +

+
+ + {appointment.status} + +
+ +
+
+ + {appointment.date} +
+
+ + {appointment.time} +
+
+
+ ))} +
-
- ))} -
+ -
-
-
-

- Upcoming Appointments -

- -
- -
- {appointments.map(appointment => ( -
-
-
-

- {appointment.doctor} -

-

- {appointment.type} -

-
-
- {appointment.status} -
-
-
-
- - - {appointment.date} - -
-
- - - {appointment.time} - -
-
+ {/* Hospitals Card */} + +
+
+

+ Nearby Hospitals +

+ + + View Map +
- ))} -
-
-
-
-

- Nearby Hospitals -

- -
+
+ {hospitals.map((hospital, index) => ( + +
+
+

+ {hospital.name} +

+

+ {hospital.distance} away +

+
+ + {hospital.availability} + +
-
- {hospitals.map(hospital => ( -
-
-
-

- {hospital.name} -

-

- {hospital.distance} away -

-
-
- {hospital.availability} -
-
- + + View Details + + + + ))}
- ))} -
+
+
-
-
-

- Health Summary -

-
-
-

- Latest Vitals -

-
- {[ + {/* Health Summary Section */} + +

+ Health Summary +

+ +
+ {/* Latest Vitals */} + ( -
- - {vital.label} - - - {vital.value} - -
- ))} -
+ ]} + /> + + {/* Current Medications */} + + + {/* Upcoming Tests */} +
+ -
-

- Current Medications -

-
- {[ - { name: 'Amoxicillin', dosage: '500mg', frequency: 'Twice daily' }, - { name: 'Lisinopril', dosage: '10mg', frequency: 'Once daily' }, - { name: 'Metformin', dosage: '850mg', frequency: 'With meals' } - ].map((med, index) => ( -
-
- {med.name} -
-
- {med.dosage} - {med.frequency} -
-
- ))} -
+ {/* API Data Section */} + +
+

+ Health Metrics +

+ + + Refresh +
-
-

- Upcoming Tests -

-
- {[ - { name: 'Blood Work', date: '2024-01-20', time: '9:00 AM' }, - { name: 'X-Ray', date: '2024-01-25', time: '2:30 PM' }, - { name: 'ECG', date: '2024-02-01', time: '11:15 AM' } - ].map((test, index) => ( -
-
- {test.name} -
-
- {test.date} at {test.time} -
-
- ))} + {loading ? ( +
+
-
-
+ ) : error ? ( +
+ {error} +
+ ) : ( +
+                  {JSON.stringify(healthData, null, 2)}
+                
+ )} +
-
-
+ +
+ + {/* Theme transition styles */} +
); }; +// Health Card Component +const HealthCard = ({ title, icon: Icon, data, isCompact }) => ( + +
+ +

+ {title} +

+
+ +
+ {data.map((item, index) => ( + + {isCompact ? ( + <> +
+ {item.name} +
+
+ {item.details} +
+ + ) : ( +
+ + {item.label} + + + {item.value} + +
+ )} +
+ ))} +
+
+); + export default Dashboard; \ No newline at end of file diff --git a/healthhub-frontend/src/components/basic_information.jsx b/healthhub-frontend/src/components/basic_information.jsx index fc12b3b..a10eadf 100644 --- a/healthhub-frontend/src/components/basic_information.jsx +++ b/healthhub-frontend/src/components/basic_information.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, memo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { User, Calendar, Activity, Ruler, Weight, AlertCircle } from 'lucide-react'; +import { Calendar, Activity, Ruler, Weight, AlertCircle } from 'lucide-react'; const InputField = memo(({ icon: Icon, @@ -16,7 +16,7 @@ const InputField = memo(({ isFocused, placeholder, ...props - }) => ( +}) => (
- )); - - const SelectButton = memo(({ options, name, value = '', onChange, error, touched }) => ( +)); + +const SelectButton = memo(({ options, name, value = '', onChange, error, touched }) => (
{options.map((option) => ( @@ -107,221 +107,203 @@ const InputField = memo(({ )}
- )); - +)); + const BasicInfoStep = ({ - data = { - fullName: '', - dateOfBirth: '', - gender: '', - bloodType: '', - height: '', - weight: '' - }, - onChange = () => {}, - onValidationChange = () => {} + data = { + dateOfBirth: '', + gender: '', + bloodType: '', + height: '', + weight: '' + }, + onChange = () => {}, + onValidationChange = () => {} }) => { - const [errors, setErrors] = useState({}); - const [touched, setTouched] = useState({}); - const [focusedField, setFocusedField] = useState(null); - const [validationMessage, setValidationMessage] = useState(null); + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [focusedField, setFocusedField] = useState(null); + const [validationMessage, setValidationMessage] = useState(null); - const validateField = (name, value) => { - switch (name) { - case 'fullName': - if (!value?.trim()) return 'Full name is required'; - if (value.length < 2) return 'Name is too short'; - if (!/^[a-zA-Z\s'-]+$/.test(value)) return 'Please enter a valid name'; - return null; - case 'dateOfBirth': - if (!value) return 'Date of birth is required'; - const date = new Date(value); - const age = new Date().getFullYear() - date.getFullYear(); - if (age < 0 || age > 120) return 'Invalid date'; - return null; - case 'height': - if (!value) return 'Height is required'; - const height = parseFloat(value); - if (isNaN(height) || height < 30 || height > 250) return 'Please enter a valid height'; - return null; - case 'weight': - if (!value) return 'Weight is required'; - const weight = parseFloat(value); - if (isNaN(weight) || weight < 20 || weight > 300) return 'Please enter a valid weight'; - return null; - case 'gender': - return !value ? 'Please select a gender' : null; - case 'bloodType': - return !value ? 'Please select blood type' : null; - default: - return null; - } - }; + const validateField = (name, value) => { + switch (name) { + case 'dateOfBirth': + if (!value) return 'Date of birth is required'; + const date = new Date(value); + const age = new Date().getFullYear() - date.getFullYear(); + if (age < 0 || age > 120) return 'Invalid date'; + return null; + case 'height': + if (!value) return 'Height is required'; + const height = parseFloat(value); + if (isNaN(height) || height < 30 || height > 250) return 'Please enter a valid height'; + return null; + case 'weight': + if (!value) return 'Weight is required'; + const weight = parseFloat(value); + if (isNaN(weight) || weight < 20 || weight > 300) return 'Please enter a valid weight'; + return null; + case 'gender': + return !value ? 'Please select a gender' : null; + case 'bloodType': + return !value ? 'Please select blood type' : null; + default: + return null; + } + }; - const handleChange = (name, value) => { - const newData = { ...data, [name]: value }; - const error = validateField(name, value); - - setErrors(prev => ({ - ...prev, - [name]: error - })); - - setTouched(prev => ({ - ...prev, - [name]: true - })); + const handleChange = (name, value) => { + const newData = { ...data, [name]: value }; + const error = validateField(name, value); + + setErrors(prev => ({ + ...prev, + [name]: error + })); + + setTouched(prev => ({ + ...prev, + [name]: true + })); - onChange(newData); - validateForm(newData); - }; + onChange(newData); + validateForm(newData); + }; - const handleFocus = (name) => { - setFocusedField(name); - }; + const handleFocus = (name) => { + setFocusedField(name); + }; - const handleBlur = (name) => { - setFocusedField(null); - const error = validateField(name, data[name]); - setErrors(prev => ({ - ...prev, - [name]: error - })); - }; + const handleBlur = (name) => { + setFocusedField(null); + const error = validateField(name, data[name]); + setErrors(prev => ({ + ...prev, + [name]: error + })); + }; - const validateForm = (formData) => { - const newErrors = {}; - let hasErrors = false; - - Object.keys(formData).forEach(field => { - const error = validateField(field, formData[field]); - if (error) { - newErrors[field] = error; - hasErrors = true; - } - }); + const validateForm = (formData) => { + const newErrors = {}; + let hasErrors = false; + + // Check each required field + ['dateOfBirth', 'gender', 'bloodType', 'height', 'weight'].forEach(field => { + const error = validateField(field, formData[field]); + if (error) { + newErrors[field] = error; + hasErrors = true; + } + }); - setErrors(newErrors); - setValidationMessage(hasErrors ? 'Please fill in all required fields marked with an asterisk (*)' : null); - onValidationChange?.(!hasErrors); - }; + setErrors(newErrors); + setValidationMessage(hasErrors ? 'Please fill in all required fields marked with an asterisk (*)' : null); + onValidationChange?.(!hasErrors); + }; - useEffect(() => { - validateForm(data); - }, []); + // Run initial validation + useEffect(() => { + validateForm(data); + }, []); - return ( -
-
- + return ( +
+
+ - +
+ + +
-
- - -
+
+ -
- + +
- -
+
+ + +
+
-
- - + + {validationMessage && ( + + + {validationMessage} + + )} +
-
- - - {validationMessage && ( - - - {validationMessage} - - )} - -
- ); + ); }; export default BasicInfoStep; \ No newline at end of file diff --git a/healthhub-frontend/src/components/contact_detail.jsx b/healthhub-frontend/src/components/contact_detail.jsx index 6d824fe..691be87 100644 --- a/healthhub-frontend/src/components/contact_detail.jsx +++ b/healthhub-frontend/src/components/contact_detail.jsx @@ -75,8 +75,6 @@ const InputField = memo(({ const ContactDetailsStep = ({ data = { - email: '', - phone: '', address: { street: '', city: '', @@ -93,15 +91,11 @@ const ContactDetailsStep = ({ const [focusedField, setFocusedField] = useState(null); const [validationMessage, setValidationMessage] = useState(null); - const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const PHONE_PATTERN = /^\+?[\d\s-]{10,}$/; const POSTAL_CODE_PATTERN = /^[A-Z\d]{3,10}$/i; const validateField = (name, value) => { if (!value?.trim()) { - if (name === 'email' || - name === 'phone' || - name === 'address.street' || + if (name === 'address.street' || name === 'address.city' || name === 'address.country') { return `${name.split('.').pop().charAt(0).toUpperCase() + name.split('.').pop().slice(1)} is required`; @@ -109,16 +103,6 @@ const ContactDetailsStep = ({ } switch (name) { - case 'email': - if (!EMAIL_PATTERN.test(value)) { - return 'Please enter a valid email address'; - } - break; - case 'phone': - if (!PHONE_PATTERN.test(value)) { - return 'Please enter a valid phone number'; - } - break; case 'address.postalCode': if (value && !POSTAL_CODE_PATTERN.test(value)) { return 'Please enter a valid postal code'; @@ -181,14 +165,6 @@ const ContactDetailsStep = ({ const newErrors = {}; let hasErrors = false; - ['email', 'phone'].forEach(field => { - const error = validateField(field, formData[field]); - if (error) { - newErrors[field] = error; - hasErrors = true; - } - }); - ['street', 'city', 'state', 'postalCode', 'country'].forEach(field => { const error = validateField(`address.${field}`, formData.address?.[field]); if (error) { @@ -214,40 +190,6 @@ const ContactDetailsStep = ({ animate={{ opacity: 1, y: 0 }} className="space-y-6" > -
- - - -
-
diff --git a/healthhub-frontend/src/components/medication_details.jsx b/healthhub-frontend/src/components/medication_details.jsx index 8e5c123..b83e803 100644 --- a/healthhub-frontend/src/components/medication_details.jsx +++ b/healthhub-frontend/src/components/medication_details.jsx @@ -3,7 +3,9 @@ import { motion, AnimatePresence } from 'framer-motion'; import { User, ChevronRight, ChevronLeft, Activity, Heart, Phone, MapPin, AlertCircle, Calendar, Clock, - Plus, Minus, Check, ArrowRight, Sparkles, X + Plus, Minus, Check, ArrowRight, Sparkles, X, + House, + Building } from 'lucide-react'; import BasicInfoStep from './basic_information'; @@ -24,7 +26,6 @@ export const FORM_STEPS = [ component: BasicInfoStep, validateStep: (data) => { return Boolean( - data.fullName?.trim() && data.dateOfBirth && data.gender && data.bloodType @@ -33,15 +34,13 @@ export const FORM_STEPS = [ }, { id: 'contact', - title: 'Contact Details', + title: 'Adderess Details', subtitle: 'How can we reach you?', - icon: Phone, + icon: Building, required: true, component: ContactDetailsStep, validateStep: (data) => { return Boolean( - data.email?.trim() && - data.phone?.trim() && data.address?.street?.trim() && data.address?.city?.trim() && data.address?.country?.trim() diff --git a/healthhub-frontend/src/components/review.jsx b/healthhub-frontend/src/components/review.jsx index 328ad4a..f7eb925 100644 --- a/healthhub-frontend/src/components/review.jsx +++ b/healthhub-frontend/src/components/review.jsx @@ -6,14 +6,11 @@ import { const ReviewStep = ({ data = { - fullName: '', dateOfBirth: '', gender: '', bloodType: '', height: '', weight: '', - email: '', - phone: '', address: {}, emergencyContacts: [], allergies: [], @@ -78,7 +75,6 @@ const ReviewStep = ({ ); const isBasicInfoComplete = Boolean( - data.fullName && data.dateOfBirth && data.gender && data.bloodType && @@ -87,8 +83,6 @@ const ReviewStep = ({ ); const isContactComplete = Boolean( - data.email && - data.phone && data.address?.street && data.address?.city && data.address?.country diff --git a/healthhub-frontend/src/styles/animations.css b/healthhub-frontend/src/styles/animations.css index 0a3e703..9201bc3 100644 --- a/healthhub-frontend/src/styles/animations.css +++ b/healthhub-frontend/src/styles/animations.css @@ -176,4 +176,4 @@ animation: heartbeat-move 15s linear infinite, heartbeat-opacity 3s ease-in-out infinite; - } \ No newline at end of file + } diff --git a/healthhub-frontend/tailwind.config.js b/healthhub-frontend/tailwind.config.js index eafe0d9..8452104 100644 --- a/healthhub-frontend/tailwind.config.js +++ b/healthhub-frontend/tailwind.config.js @@ -27,4 +27,32 @@ export default { }, plugins: [], } + module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}"], + darkMode: 'class', + theme: { + extend: {}, + }, + safelist: [ + 'bg-blue-100', + 'bg-teal-100', + 'bg-purple-100', + 'bg-pink-100', + 'text-blue-500', + 'text-teal-500', + 'text-purple-500', + 'text-pink-500', + 'dark:bg-blue-900/30', + 'dark:bg-teal-900/30', + 'dark:bg-purple-900/30', + 'dark:bg-pink-900/30', + 'dark:text-blue-400', + 'dark:text-teal-400', + 'dark:text-purple-400', + 'dark:text-pink-400', + ], + plugins: [], + } + + \ No newline at end of file