diff --git a/.Jules/changelog.md b/.Jules/changelog.md index ecc0f8a2..11fc864e 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,15 @@ ## [Unreleased] ### Added +- **Password Strength Meter:** Added a visual password strength indicator to the signup form. + - **Features:** + - Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol). + - Visual feedback with segmented progress bar and color coding. + - Specific criteria checklist (6+ chars, Mixed case, Number, Symbol). + - Dual-theme support (Neobrutalism & Glassmorphism). + - Accessible ARIA live region for screen readers. + - **Technical:** Created `web/components/ui/PasswordStrength.tsx`. Integrated into `web/pages/Auth.tsx`. + - **Mobile Haptics:** Implemented system-wide haptic feedback for all interactive elements. - **Features:** - Created `HapticButton`, `HapticIconButton`, `HapticFAB`, `HapticCard`, `HapticList`, `HapticCheckbox`, `HapticMenu`, `HapticSegmentedButtons`, `HapticAppbar` (including `HapticAppbarAction`, `HapticAppbarBackAction`) wrappers. diff --git a/.Jules/todo.md b/.Jules/todo.md index ccd795e4..ebb0c7a5 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -164,3 +164,7 @@ - Completed: 2026-01-21 - Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`, `mobile/screens/FriendsScreen.js` - Impact: Native feel, users can easily refresh data +- [x] **[polish]** Password strength meter for signup + - Completed: 2026-02-08 + - Files modified: `web/components/ui/PasswordStrength.tsx`, `web/pages/Auth.tsx` + - Impact: Provides visual feedback on password complexity during signup diff --git a/web/components/ui/PasswordStrength.tsx b/web/components/ui/PasswordStrength.tsx new file mode 100644 index 00000000..605fd618 --- /dev/null +++ b/web/components/ui/PasswordStrength.tsx @@ -0,0 +1,127 @@ +import React, { useMemo } from 'react'; +import { useTheme } from '../../contexts/ThemeContext'; +import { THEMES } from '../../constants'; +import { Check } from 'lucide-react'; + +interface PasswordStrengthProps { + password?: string; +} + +export const PasswordStrength: React.FC = ({ password = '' }) => { + const { style } = useTheme(); + + const { score, label, metCriteria } = useMemo(() => { + let s = 0; + const criteria = { + length: password.length >= 6, + hasUpper: /[A-Z]/.test(password), + hasLower: /[a-z]/.test(password), + hasNumber: /[0-9]/.test(password), + hasSpecial: /[^A-Za-z0-9]/.test(password), + }; + + let l = 'Weak'; + + if (password.length > 0) { + if (password.length < 6) { + s = 1; // Very Weak + l = 'Too Short'; + } else { + s = 1; + // Bonus for length + if (criteria.length) s++; + + // Bonus for complexity + const complexity = (criteria.hasUpper ? 1 : 0) + + (criteria.hasLower ? 1 : 0) + + (criteria.hasNumber ? 1 : 0) + + (criteria.hasSpecial ? 1 : 0); + + if (complexity >= 2) s++; + if (complexity >= 4) s++; // Max bonus + + // Adjust label based on final score + if (s >= 4) l = 'Strong'; + else if (s === 3) l = 'Good'; + else if (s === 2) l = 'Fair'; + else l = 'Weak'; + } + } else { + l = ''; + } + + // Normalize score 0-4 + if (s > 4) s = 4; + + return { score: s, label: l, metCriteria: criteria }; + }, [password]); + + const getColor = (s: number) => { + switch (s) { + case 1: return 'bg-red-500'; + case 2: return 'bg-orange-500'; + case 3: return 'bg-yellow-500'; + case 4: return 'bg-green-500'; + default: return 'bg-gray-200 dark:bg-zinc-700'; + } + }; + + const isNeo = style === THEMES.NEOBRUTALISM; + + if (!password) return null; + + return ( +
+ {/* Strength Bar */} +
+ {[1, 2, 3, 4].map((level) => ( +
= level + ? getColor(score) + : (isNeo ? 'bg-gray-200 dark:bg-zinc-800' : 'bg-white/10') + } ${isNeo ? 'border-2 border-black' : 'rounded-full'}`} + /> + ))} +
+ + {/* Label and Criteria */} +
+ + {label} + +
+ +
+
+ {metCriteria.length ? :
} + 6+ characters +
+
+ {(metCriteria.hasUpper && metCriteria.hasLower) ? :
} + Mixed case +
+
+ {metCriteria.hasNumber ? :
} + Number +
+
+ {metCriteria.hasSpecial ? :
} + Symbol +
+
+ + {/* Hidden live region for accessibility */} + + Password strength: {label}. + {score < 4 && label ? "Add more characters, numbers, or symbols to strengthen." : ""} + +
+ ); +}; diff --git a/web/pages/Auth.tsx b/web/pages/Auth.tsx index 4f35a289..5f0efbee 100644 --- a/web/pages/Auth.tsx +++ b/web/pages/Auth.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '../components/ui/Button'; import { Input } from '../components/ui/Input'; +import { PasswordStrength } from '../components/ui/PasswordStrength'; import { Spinner } from '../components/ui/Spinner'; import { THEMES } from '../constants'; import { useAuth } from '../contexts/AuthContext'; @@ -299,6 +300,8 @@ export const Auth = () => { className={isNeo ? 'rounded-none' : ''} /> + {!isLogin && } + {error && (