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)',