diff --git a/client/package-lock.json b/client/package-lock.json index 843edde..08bcf65 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,11 +14,13 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.11.0", + "framer-motion": "^12.23.26", "jwt-decode": "^4.0.0", "lucide-react": "^0.526.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-intersection-observer": "^10.0.0", "react-router-dom": "^7.6.3", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4" @@ -8502,6 +8504,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -11636,6 +11665,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -14084,6 +14128,21 @@ "react": "*" } }, + "node_modules/react-intersection-observer": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.0.tgz", + "integrity": "sha512-JJRgcnFQoVXmbE5+GXr1OS1NDD1gHk0HyfpLcRf0575IbJz+io8yzs4mWVlfaqOQq1FiVjLvuYAdEEcrrCfveg==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/client/package.json b/client/package.json index 2c5d2ab..4a759d0 100644 --- a/client/package.json +++ b/client/package.json @@ -9,11 +9,13 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.11.0", + "framer-motion": "^12.23.26", "jwt-decode": "^4.0.0", "lucide-react": "^0.526.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-intersection-observer": "^10.0.0", "react-router-dom": "^7.6.3", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4" diff --git a/client/src/App.js b/client/src/App.js index dbdfff2..7fb551e 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import "./components/styles.css"; import { Routes, Route } from "react-router-dom"; +import { AnimatePresence } from "framer-motion"; import HomePage from "./pages/HomePage"; import PharmacyPage from "./pages/PharmacyPage"; import LabsPage from "./pages/LabsPage"; @@ -13,14 +14,53 @@ import EmergencyPanel from "./components/EmergencyPanel"; import ForgotPassword from "./pages/ForgotPassword"; import ProtectedRoute from "./components/ProtectedRoute"; import AdminDashboardPage from "./pages/AdminDashboardPage"; -import MedicineListing from "./pages/Medicine"; // <-- import the medicine page +import MedicineListing from "./pages/Medicine"; +import CustomCursor from "./components/CustomCursor"; + +// Check if device supports hover (non-touch devices) +const useIsTouchDevice = () => { + const [isTouchDevice, setIsTouchDevice] = useState(false); + + useEffect(() => { + const checkTouchDevice = () => { + setIsTouchDevice( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ); + }; + + checkTouchDevice(); + window.addEventListener('resize', checkTouchDevice); + return () => window.removeEventListener('resize', checkTouchDevice); + }, []); + + return isTouchDevice; +}; function App() { const [darkMode, setDarkMode] = useState(false); const [isEmergencyOpen, setIsEmergencyOpen] = useState(false); + const isTouchDevice = useIsTouchDevice(); + const location = window.location; useEffect(() => { document.body.classList.toggle("dark-mode", darkMode); + + // Add smooth scrolling for anchor links + const smoothScroll = (e) => { + const targetId = e.target.getAttribute('href'); + if (targetId && targetId.startsWith('#')) { + e.preventDefault(); + const targetElement = document.querySelector(targetId); + if (targetElement) { + targetElement.scrollIntoView({ behavior: 'smooth' }); + } + } + }; + + document.addEventListener('click', smoothScroll); + return () => document.removeEventListener('click', smoothScroll); }, [darkMode]); const toggleEmergencyPanel = () => { @@ -29,26 +69,29 @@ function App() { return ( <> - - } /> - } /> - } /> - } /> - } /> {/* new route */} - } /> - } /> - } /> - } /> - - - - } - /> - } /> - + {!isTouchDevice && } + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + } /> + + {/* Fixed Emergency Button */} setIsEmergencyOpen(false)} + onClose={toggleEmergencyPanel} /> > ); diff --git a/client/src/components/CustomCursor.js b/client/src/components/CustomCursor.js new file mode 100644 index 0000000..d49e8e2 --- /dev/null +++ b/client/src/components/CustomCursor.js @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import styles from './CustomCursor.module.css'; + +const CustomCursor = () => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isHovered, setIsHovered] = useState(false); + const [isClicked, setIsClicked] = useState(false); + + useEffect(() => { + const mouseMove = (e) => { + setPosition({ x: e.clientX, y: e.clientY }); + + // Check if hovering over interactive elements + const target = e.target; + const isInteractive = + target.tagName === 'A' || + target.tagName === 'BUTTON' || + target.closest('button') || + target.closest('a') || + target.getAttribute('role') === 'button' || + target.hasAttribute('data-hover-effect'); + + setIsHovered(isInteractive); + }; + + const mouseDown = () => setIsClicked(true); + const mouseUp = () => setIsClicked(false); + + document.addEventListener('mousemove', mouseMove); + document.addEventListener('mousedown', mouseDown); + document.addEventListener('mouseup', mouseUp); + + // Add hover effect to all interactive elements + const interactiveElements = document.querySelectorAll( + 'a, button, [role="button"], [data-hover-effect]' + ); + + interactiveElements.forEach(el => { + el.setAttribute('data-hover-effect', 'true'); + }); + + return () => { + document.removeEventListener('mousemove', mouseMove); + document.removeEventListener('mousedown', mouseDown); + document.removeEventListener('mouseup', mouseUp); + }; + }, []); + + return ( + + + + ); +}; + +export default CustomCursor; diff --git a/client/src/components/CustomCursor.module.css b/client/src/components/CustomCursor.module.css new file mode 100644 index 0000000..811aecf --- /dev/null +++ b/client/src/components/CustomCursor.module.css @@ -0,0 +1,57 @@ +.cursor { + position: fixed; + width: 20px; + height: 20px; + border-radius: 50%; + pointer-events: none; + z-index: 9999; + background-color: transparent; + border: 2px solid #4f46e5; + transition: transform 0.15s ease, width 0.2s ease, height 0.2s ease; + transform: translate(-50%, -50%); + will-change: transform; + mix-blend-mode: difference; +} + +.cursorInner { + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #4f46e5; + opacity: 0.6; + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.hovered { + transform: translate(-50%, -50%) scale(1.5); + border-color: #7c3aed; +} + +.hovered .cursorInner { + transform: scale(0.5); + background-color: #7c3aed; + opacity: 0.8; +} + +.clicked { + transform: translate(-50%, -50%) scale(0.8); +} + +.clicked .cursorInner { + transform: scale(1.2); +} + +/* Hide cursor when not needed */ +@media (pointer: coarse) { + .cursor { + display: none; + } +} + +/* Hide cursor on touch devices */ +@media (hover: none) { + .cursor { + display: none; + } +} diff --git a/client/src/components/HeroSection.js b/client/src/components/HeroSection.js index 57fe100..093e4bb 100644 --- a/client/src/components/HeroSection.js +++ b/client/src/components/HeroSection.js @@ -1,20 +1,125 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { motion, useAnimation } from 'framer-motion'; +import { useInView } from 'react-intersection-observer'; import styles from './HeroSection.module.css'; import pic1 from '../assets/pic5.webp'; +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.3, + delayChildren: 0.2 + } + } +}; + +const itemVariants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: 'spring', + damping: 12, + stiffness: 100 + } + } +}; + +const imageVariants = { + hidden: { scale: 0.9, opacity: 0 }, + visible: { + scale: 1, + opacity: 1, + transition: { + type: 'spring', + damping: 15, + stiffness: 100, + delay: 0.3 + } + }, + hover: { + scale: 1.02, + transition: { duration: 0.3 } + } +}; + const HeroSection = () => { + const controls = useAnimation(); + const [ref, inView] = useInView({ + triggerOnce: true, + threshold: 0.1 + }); + + useEffect(() => { + if (inView) { + controls.start('visible'); + } + }, [controls, inView]); + return ( - - - - - - - - Welcome to MidCity Urology and General Nursing Home — your trusted center for compassionate, reliable urological and general healthcare. - - - + + + + + + + + Welcome to{' '} + + MidCity Urology and General Nursing Home + {' '} + — your trusted center for compassionate, reliable urological and general healthcare. + + + ); }; diff --git a/client/src/components/styles.css b/client/src/components/styles.css index af7ea4e..3d66341 100644 --- a/client/src/components/styles.css +++ b/client/src/components/styles.css @@ -1,31 +1,317 @@ +/* Base Styles */ +:root { + --primary: #4f46e5; + --primary-hover: #4338ca; + --secondary: #7c3aed; + --text: #1f2937; + --text-light: #6b7280; + --bg: #ffffff; + --bg-secondary: #f9fafb; + --border: #e5e7eb; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Dark Mode */ +[data-theme='dark'] { + --primary: #6366f1; + --primary-hover: #818cf8; + --text: #f3f4f6; + --text-light: #9ca3af; + --bg: #111827; + --bg-secondary: #1f2937; + --border: #374151; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + body { - font-family: Arial, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; margin: 0; padding: 0; - scroll-behavior: smooth; + background-color: var(--bg); + color: var(--text); + line-height: 1.6; + transition: background-color 0.3s ease, color 0.3s ease; + overflow-x: hidden; +} + +a { + color: var(--primary); + text-decoration: none; + transition: var(--transition); +} + +a:hover { + color: var(--primary-hover); +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: 1rem; } +/* Animation Classes */ +.fade-in { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.slide-up { + animation: slideUp 0.5s ease-out; +} + +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--primary-hover); +} + +/* Header Styles */ .header_container { display: flex; - width: 100vw; + width: 100%; flex-direction: row; align-items: center; justify-content: space-between; + min-height: 80px; + padding: 0.75rem 2rem; + background-color: var(--bg); + border-bottom: 1px solid var(--border); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + transition: var(--transition); + box-shadow: var(--shadow); +} + +/* Scrolled Header */ +.header-scrolled { + padding: 0.5rem 2rem; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +/* Header Logo */ +.logo-container { + display: flex; + align-items: center; + transition: var(--transition); +} + +.logo-img { + height: 50px; + width: auto; + transition: var(--transition); +} - height: 80px; +.header-scrolled .logo-img { + height: 40px; +} + +/* Navigation Menu */ +.nav_menu { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.nav_link { + display: flex; + gap: 2rem; + list-style: none; + margin: 0; + padding: 0; +} + +.nav_link a { + color: var(--text); + font-weight: 500; + padding: 0.5rem 0; + position: relative; + text-decoration: none; + transition: var(--transition); +} + +.nav_link a::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background-color: var(--primary); + transition: var(--transition); +} + +.nav_link a:hover::after, +.nav_link a.active::after { width: 100%; +} + +/* Mobile Menu Toggle */ +.mobile-menu-btn { + display: none; + background: none; + border: none; + color: var(--text); + font-size: 1.5rem; + cursor: pointer; + padding: 0.5rem; + z-index: 1001; +} - min-height: 80px; /* Changed from height: 100px */ +/* Responsive Navigation */ +@media (max-width: 1024px) { + .mobile-menu-btn { + display: block; + } - padding: 10px 20px; - background-color: #ffffff; - opacity: 0.9; - border-radius: 0px; - box-shadow: 0 0 10px rgba(0, 128, 0, 0.5); + .nav_menu { + position: fixed; + top: 0; + right: -100%; + width: 80%; + max-width: 300px; + height: 100vh; + background-color: var(--bg); + flex-direction: column; + justify-content: center; + align-items: center; + transition: var(--transition); + box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1); + z-index: 1000; + } + + .nav_menu.active { + right: 0; + } + + .nav_link { + flex-direction: column; + align-items: center; + gap: 2rem; + } +} + +/* Dark Mode Toggle */ +.theme-toggle { + background: none; + border: none; + color: var(--text); + font-size: 1.25rem; + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + transition: var(--transition); + border-radius: 50%; + width: 40px; + height: 40px; +} + +.theme-toggle:hover { + background-color: var(--bg-secondary); +} + +/* Emergency Button */ +.fixed-emergency-btn { position: fixed; - top: 0; - height: 120px; - z-index: 1000; + bottom: 2rem; + right: 2rem; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + color: white; + border: none; + border-radius: 50px; + padding: 0.75rem 1.5rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 15px rgba(79, 70, 229, 0.3); + display: flex; + align-items: center; + gap: 0.5rem; + z-index: 999; + transition: var(--transition); + animation: pulse 2s infinite; +} + +.fixed-emergency-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4); +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(79, 70, 229, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); + } +} + +/* Page Transitions */ +.page-enter { + opacity: 0; + transform: translateY(20px); +} + +.page-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity 300ms, transform 300ms; +} + +.page-exit { + opacity: 1; + transform: translateY(0); +} + +.page-exit-active { + opacity: 0; + transform: translateY(-20px); + transition: opacity 300ms, transform 300ms; } .img {
- Welcome to MidCity Urology and General Nursing Home — your trusted center for compassionate, reliable urological and general healthcare. -