From 8ee9046bb332eeeb35de9fb4a1ae37a25e2908c7 Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Wed, 11 Feb 2026 00:54:48 +0000 Subject: [PATCH] Task 3: Build LoginScreen component with typing animation - Created LoginScreen.tsx with character-by-character username typing (30ms/char) - Password dots fill at 20ms per dot - Button shows pressed state before transition - Added 'login' phase to App.tsx flow - Added PMR colors and fonts to tailwind.config.js - Added Inter font family to index.html - Respects prefers-reduced-motion: instant completion in ~500ms --- index.html | 3 +- src/App.tsx | 7 +- src/components/LoginScreen.tsx | 194 +++++++++++++++++++++++++++++++++ src/types/index.ts | 2 +- tailwind.config.js | 11 ++ 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/components/LoginScreen.tsx diff --git a/index.html b/index.html index 33c14fb..745bab1 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,8 @@ Andy Charlwood — MPharm | CV - + +
diff --git a/src/App.tsx b/src/App.tsx index 3b7ee44..32c945a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import type { Phase } from './types' import { BootSequence } from './components/BootSequence' import { ECGAnimation } from './components/ECGAnimation' +import { LoginScreen } from './components/LoginScreen' import { FloatingNav } from './components/FloatingNav' import { Hero } from './components/Hero' import { Skills } from './components/Skills' @@ -21,7 +22,11 @@ function App() { )} {phase === 'ecg' && ( - setPhase('content')} /> + setPhase('login')} /> + )} + + {phase === 'login' && ( + setPhase('content')} /> )} {phase === 'content' && ( diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx new file mode 100644 index 0000000..5ef3ae5 --- /dev/null +++ b/src/components/LoginScreen.tsx @@ -0,0 +1,194 @@ +import { useState, useEffect, useCallback } from 'react' +import { Shield } from 'lucide-react' + +interface LoginScreenProps { + onComplete: () => void +} + +export function LoginScreen({ onComplete }: LoginScreenProps) { + const [username, setUsername] = useState('') + const [passwordDots, setPasswordDots] = useState(0) + const [showCursor, setShowCursor] = useState(true) + const [isTypingUsername, setIsTypingUsername] = useState(true) + const [isTypingPassword, setIsTypingPassword] = useState(false) + const [buttonPressed, setButtonPressed] = useState(false) + + const fullUsername = 'A.CHARLWOOD' + const passwordLength = 8 + + const prefersReducedMotion = typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false + + const startLoginSequence = useCallback(() => { + if (prefersReducedMotion) { + setUsername(fullUsername) + setPasswordDots(passwordLength) + setTimeout(() => { + setButtonPressed(true) + setTimeout(onComplete, 200) + }, 300) + return + } + + setIsTypingUsername(true) + let usernameIndex = 0 + + const usernameInterval = setInterval(() => { + if (usernameIndex <= fullUsername.length) { + setUsername(fullUsername.slice(0, usernameIndex)) + usernameIndex++ + } else { + clearInterval(usernameInterval) + setIsTypingUsername(false) + setIsTypingPassword(true) + + setTimeout(() => { + let dotCount = 0 + const passwordInterval = setInterval(() => { + if (dotCount <= passwordLength) { + setPasswordDots(dotCount) + dotCount++ + } else { + clearInterval(passwordInterval) + setIsTypingPassword(false) + + setTimeout(() => { + setButtonPressed(true) + setTimeout(onComplete, 200) + }, 150) + } + }, 20) + }, 150) + } + }, 30) + }, [onComplete, prefersReducedMotion]) + + useEffect(() => { + const cursorInterval = setInterval(() => { + setShowCursor(prev => !prev) + }, 530) + + startLoginSequence() + + return () => clearInterval(cursorInterval) + }, [startLoginSequence]) + + return ( +
+
+
+
+ +
+ + CareerRecord PMR + +
+ +
+
+ +
+ {username} + {isTypingUsername && ( + + | + + )} +
+
+ +
+ +
+ {'\u2022'.repeat(passwordDots)} + {isTypingPassword && ( + + | + + )} +
+
+ + +
+ +
+

+ Secure clinical system login +

+
+
+
+ ) +} diff --git a/src/types/index.ts b/src/types/index.ts index fcad70d..2d46edd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -33,7 +33,7 @@ export interface ContactItem { href?: string } -export type Phase = 'boot' | 'ecg' | 'content' +export type Phase = 'boot' | 'ecg' | 'login' | 'content' export interface BootLine { html: string diff --git a/tailwind.config.js b/tailwind.config.js index 1fb40c5..c83a0de 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -33,11 +33,22 @@ export default { dim: '#3a6b45', grey: '#666666', }, + pmr: { + sidebar: '#1E293B', + banner: '#334155', + content: '#F5F7FA', + nhsblue: '#005EB8', + green: '#22C55E', + amber: '#F59E0B', + red: '#EF4444', + }, }, fontFamily: { primary: ['Plus Jakarta Sans', 'system-ui', 'sans-serif'], secondary: ['Inter Tight', 'system-ui', 'sans-serif'], mono: ['Fira Code', 'monospace'], + inter: ['Inter', 'system-ui', 'sans-serif'], + geist: ['Geist Mono', 'Fira Code', 'monospace'], }, boxShadow: { 'sm': '0 1px 3px rgba(0,0,0,0.06)',