From 8bbc106284dd59db9c3ad12b69500867b5ec07a0 Mon Sep 17 00:00:00 2001 From: Kirti Pant Date: Thu, 31 Jul 2025 20:04:43 +0530 Subject: [PATCH] Designed Login modal component with accessibility, validation, and Gravatar integration in main website --- LoginReadme.md | 152 +++++++++++++++++++ src/components/Login.css | 201 +++++++++++++++++++++++++ src/components/Login.tsx | 311 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 664 insertions(+) create mode 100644 LoginReadme.md create mode 100644 src/components/Login.css create mode 100644 src/components/Login.tsx diff --git a/LoginReadme.md b/LoginReadme.md new file mode 100644 index 0000000..2b11680 --- /dev/null +++ b/LoginReadme.md @@ -0,0 +1,152 @@ +# React Login Modal Component + +A modern, accessible, animated React login modal featuring: + +- Email & Password inputs with strong validation +- "Show Password" toggle +- Gravatar-based user avatars +- Animated SVG logo and heading +- User greeting on successful login +- Toast notifications and redirect after login +- Smooth modal transitions +- Easy integration in any layout + +--- + +## Features + + +## Features + +- **Login Form & Accessibility:** + Collects Email and Password with proper labels, screen-reader text, and ARIA roles for inclusive usage. + +- **Password Validation:** + Enforces minimum 8 characters, uppercase, lowercase, digit, and special characters. + +- **Show/Hide Password:** + Adjacent button toggles password input visibility for better usability. + +- **Gravatar Avatars:** + Displays user's avatar using their email via Gravatar, with fallback to default icon. + +- **Animated UI:** + Animated SVG logo, gradient headings, and fade-in effects for greeting and feedback. All controlled via CSS (`Login.css`). + +- **Post-login Greeting:** + Friendly "Hi, [username]!" message and avatar for 2 seconds after success. + +- **Toast Notification:** + Uses `react-hot-toast` to provide instant feedback. + +- **Navigation:** + Redirects the user to the main website’s home page after login. + +- **Fully Responsive & Accessible:** + Keyboard navigable and screen-reader friendly. + +--- + +## Files Included + +- `src/components/Login.tsx` + React component implementing the login modal UI, logic for password validation, Gravatar integration, and login workflow. + +- `src/components/Login.css` + CSS styles for the modal overlay, login card, forms, inputs, buttons, greetings, logo, and animations. + +--- + +## Installation and Setup + +1. **Install dependencies:** + +npm install react-router-dom react-hot-toast blueimp-md5 + +2. **TypeScript type definitions (for TS projects):** + +npm install --save-dev @types/react @types/react-dom + + +3. **Usage in your app:** + +Import and use the `Login` component where you want the login modal to appear. + +import Login from './components/Login'; + +const [showLogin, setShowLogin] = useState(false); + +return ( +<> + +{showLogin && setShowLogin(false)} />} + +); + + +4. **Add Toast container in your app root:** + +import { Toaster } from 'react-hot-toast'; + +function App() { +return ( +<> +{/* Your app content */} + + +); +} + + +5. **Ensure your app uses routing with `BrowserRouter`** for navigation support. + +--- + +## How It Works + +- User fills in email and password. +- "Show Password" button toggles visibility for convenience. +- Client-side validation ensures password strength. +- On submit, simulates backend login, then: + - Stores a fake JWT token to localStorage. + - Generates and displays Gravatar avatar and greeting. + - Shows success toast. +- After 2 seconds, modal closes and user is redirected to the homepage. + +--- + +## Accessibility Considerations + +- Labels are properly associated with inputs. +- ARIA roles and `aria-describedby` attributes improve screen reader experience. +- Screen reader only descriptions provide useful hints without cluttering visual UI. +- All buttons have accessible names (`aria-label`). +- Keyboard focus and interactions are fully supported. + +--- +## Customization + +- To use your company/users’ actual avatars, swap the Gravatar logic. +- Replace simulated login with real authentication API as needed. +- Use or adapt `Layout.css` for page-wide visual consistency. + +--- + +## License + +Open source under the MIT License. + +--- + +## Acknowledgments + +- [react-hot-toast](https://react-hot-toast.com) for toast notifications. +- [blueimp-md5](https://github.com/blueimp/JavaScript-MD5) for hashing emails to support Gravatar. +- [Gravatar](https://en.gravatar.com/) for providing unique user avatars based on email. +- [react-router-dom](https://reactrouter.com/) for routing and navigation. + +--- + +Feel free to open an issue or submit a pull request to improve the login experience or add features! + +Happy coding! 🚀 diff --git a/src/components/Login.css b/src/components/Login.css new file mode 100644 index 0000000..6395bfd --- /dev/null +++ b/src/components/Login.css @@ -0,0 +1,201 @@ +.login-root { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: rgba(27, 33, 54, 0.18); + z-index: 9999; +} + +.login-card { + position: relative; + width: 350px; + background: #ffffff; + border-radius: 18px; + box-shadow: 0 12px 40px rgba(59, 130, 246, 0.12), 0 1.5px 4px #8bbcff1c; + padding: 2.2rem 2rem 2rem; + display: flex; + flex-direction: column; + align-items: stretch; + animation: fadeInScale 0.5s ease forwards; +} + +@keyframes fadeInScale { + 0% { + opacity: 0; + transform: scale(0.96); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.login-header { + display: flex; + align-items: center; + gap: 0.95rem; + margin-bottom: 1.5rem; +} + +.login-logo-attractive { + filter: drop-shadow(0 4px 14px #60a5fa66); + animation: iconFloat 2.5s ease-in-out infinite alternate; + flex-shrink: 0; +} + +@keyframes iconFloat { + 0% { + transform: translateY(0) scale(1); + } + 100% { + transform: translateY(-8px) scale(1.06); + } +} + +.login-attractive-title { + font-weight: 900; + font-size: 2.5rem; + background: linear-gradient(90deg, #2563eb 60%, #60a5fa 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + + text-shadow: + 2px 2px 16px #2563eb22, + 1px 1px 8px #b8d0f78f; + margin: 0; + letter-spacing: 0.04em; + user-select: none; + animation: loginTitlePulse 2.5s 1.2s infinite alternate; +} + +@keyframes loginTitlePulse { + 0% { + text-shadow: 2px 2px 10px #2563eb22, 1px 1px 8px #b8d0f78f; + } + 100% { + text-shadow: 2px 2px 24px #2563eb66, 2px 2px 16px #c7ddffcc; + } +} + +.login-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.login-form label { + margin-bottom: 5px; + font-weight: 500; + color: #283366; +} + +.login-form input { + padding: 11px 13px; + border-radius: 7px; + border: 1.5px solid #e5e7eb; + background: #f9fafb; + font-size: 1em; + outline-color: #2563eb; + transition: border 0.18s ease; + font-family: inherit; +} + +.login-form input:focus { + border: 1.5px solid #2563eb; + background: #fff; +} + +.login-error { + color: #ef4444; + padding: 6px 0 0 0; + text-align: center; + font-size: 0.96em; + font-weight: 600; +} + +.login-submit-btn { + margin-top: 1rem; + padding: 11px 0; + background: linear-gradient(90deg, #2563eb 60%, #5176fb 100%); + color: #fff; + font-weight: 700; + border-radius: 8px; + border: none; + cursor: pointer; + font-size: 1.07em; + letter-spacing: 0.03em; + box-shadow: 0 3px 16px rgba(37, 99, 235, 0.13); + transition: background 0.16s ease; + user-select: none; + font-family: inherit; +} + +.login-submit-btn:hover, +.login-submit-btn:focus { + background: linear-gradient(90deg, #1d4ed8 60%, #2563eb 100%); + outline: none; +} + +.login-close { + position: absolute; + top: 13px; + right: 15px; + font-size: 1.6rem; + background: none; + border: none; + color: #6b7280; + font-weight: 400; + cursor: pointer; + padding: 0 7px; + transition: color 0.15s ease; + user-select: none; +} + +.login-close:hover, +.login-close:focus { + color: #ef4444; + outline: none; +} + +/* Screen reader only helper */ +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0 0 0 0) !important; + white-space: nowrap !important; + border: 0 !important; +} +.login-icon-spin { + animation: spin 1s linear infinite; + vertical-align: middle; /* Align icon with button text */ +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + + + + + + + + + + + diff --git a/src/components/Login.tsx b/src/components/Login.tsx new file mode 100644 index 0000000..8aaf4e9 --- /dev/null +++ b/src/components/Login.tsx @@ -0,0 +1,311 @@ +/*Created Login Icon Component*/ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import md5 from 'blueimp-md5'; +import { toast } from 'react-hot-toast'; +import './Login.css'; + +type LoginProps = { + onClose: () => void; +}; + +const Login: React.FC = ({ onClose }) => { + const navigate = useNavigate(); + + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [showGreeting, setShowGreeting] = useState(false); + const [gravatarUrl, setGravatarUrl] = useState(null); + + + const validatePassword = (pwd: string): string[] => { + const errors: string[] = []; + if (pwd.length < 8) errors.push('at least 8 characters'); + if (!/[A-Z]/.test(pwd)) errors.push('one uppercase letter'); + if (!/[a-z]/.test(pwd)) errors.push('one lowercase letter'); + if (!/[0-9]/.test(pwd)) errors.push('one digit'); + if (!/[!@#$%^&*()_\-+=~`[\]{}|\\:;"\'<>,.?/]/.test(pwd)) + errors.push('one special character'); + return errors; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + + if (!email || !password) { + setError('Both fields are required!'); + return; + } + + const passwordErrors = validatePassword(password); + if (passwordErrors.length > 0) { + setError(`Password must contain ${passwordErrors.join(', ')}.`); + return; + } + + setError(''); + + try { + + await new Promise((res) => setTimeout(res, 1000)); + + + const computedGravatarUrl = `https://www.gravatar.com/avatar/${md5( + email.trim().toLowerCase() + )}?s=64&d=identicon`; + setGravatarUrl(computedGravatarUrl); + localStorage.setItem('token', 'fake-jwt-token'); + + + toast.success('Login successful! Welcome back.'); + + + setShowGreeting(true); + setTimeout(() => { + setShowGreeting(false); + onClose(); + navigate('/'); + }, 2000); + } catch { + setError('Login failed due to server error. Please try again later.'); + } + }; + + return ( +
+
+ {showGreeting && gravatarUrl ? ( +
+ User avatar +
+ Hi, {email.split('@')[0] || email}! +
+ + Welcome back! + +
+ ) : ( + <> + {/* Logo and heading */} +
+ +

+ Login +

+
+ + {/* Login form */} +
+ + setEmail(e.target.value)} + required + autoComplete="email" + aria-describedby="email-desc" + /> + + Enter your email address + + + +
+ setPassword(e.target.value)} + required + autoComplete="current-password" + aria-describedby="password-desc" + style={{ flex: 1 }} + /> + +
+ + Password must be at least 8 characters and include uppercase, lowercase, + digit, and special character. + + + {error && ( +
+ {error} +
+ )} + + +
+ + )} + + {/* Close button */} + +
+
+ ); +}; + +export default Login; + + + + + + + + + + + + + + + + + + + + +