Task 2 & 3: Set up project structure and build BootSequence component

- Task 2: Project structure and types already complete from Task 1
- Task 3: Create BootSequence component with Framer Motion
  - 14 boot lines with 220ms staggered delays
  - Terminal typing animation with opacity/translateY transitions
  - Blinking cursor using CSS animation
  - AnimatePresence for 800ms exit fade
  - Total boot time ~4.28s matching concept.html
- Update App.tsx to use BootSequence with phase transitions
This commit is contained in:
2026-02-10 15:59:44 +00:00
parent 17ffd237a2
commit 77c03144a9
4 changed files with 123 additions and 6 deletions
+8 -4
View File
@@ -1,5 +1,6 @@
import { useState } from 'react'
import type { Phase } from './types'
import { BootSequence } from './components/BootSequence'
function App() {
const [phase, setPhase] = useState<Phase>('boot')
@@ -7,13 +8,16 @@ function App() {
return (
<div className="min-h-screen bg-white">
{phase === 'boot' && (
<div className="fixed inset-0 bg-black flex flex-col justify-center p-10 font-mono text-sm">
<div className="text-ecg-green">CLINICAL TERMINAL v3.2.1</div>
<BootSequence onComplete={() => setPhase('ecg')} />
)}
{phase === 'ecg' && (
<div className="fixed inset-0 bg-black flex flex-col justify-center items-center">
<button
onClick={() => setPhase('content')}
className="mt-8 text-ecg-cyan hover:text-ecg-green transition-colors"
className="text-ecg-green font-mono hover:opacity-80 transition-opacity"
>
Press to skip boot sequence (placeholder)
ECG Animation (placeholder - click to continue)
</button>
</div>
)}
+95
View File
@@ -0,0 +1,95 @@
import { motion, AnimatePresence } from 'framer-motion'
import { useEffect, useState } from 'react'
interface BootLine {
html: string
delay: number
}
const bootLines: BootLine[] = [
{ html: '<span class="text-[#00ff41] font-bold">CLINICAL TERMINAL v3.2.1</span>', delay: 0 },
{ html: '<span class="text-[#3a6b45]">Initialising pharmacist profile...</span>', delay: 220 },
{ html: '<span class="text-[#3a6b45]">---</span>', delay: 220 },
{ html: '<span class="text-[#00e5ff]">SYSTEM </span><span class="text-[#00ff41]">NHS Norfolk &amp; Waveney ICB</span>', delay: 220 },
{ html: '<span class="text-[#00e5ff]">USER </span><span class="text-[#00ff41]">Andy Charlwood</span>', delay: 220 },
{ html: '<span class="text-[#00e5ff]">ROLE </span><span class="text-[#00ff41]">Deputy Head of Population Health &amp; Data Analysis</span>', delay: 220 },
{ html: '<span class="text-[#00e5ff]">LOCATION </span><span class="text-[#00ff41]">Norwich, UK</span>', delay: 220 },
{ html: '<span class="text-[#3a6b45]">---</span>', delay: 220 },
{ html: '<span class="text-[#3a6b45]">Loading modules...</span>', delay: 220 },
{ html: '<span class="text-[#00ff41] font-bold">[OK]</span> <span class="text-[#3a6b45]">pharmacist_core.sys</span>', delay: 220 },
{ html: '<span class="text-[#00ff41] font-bold">[OK]</span> <span class="text-[#3a6b45]">population_health.mod</span>', delay: 220 },
{ html: '<span class="text-[#00ff41] font-bold">[OK]</span> <span class="text-[#3a6b45]">data_analytics.eng</span>', delay: 220 },
{ html: '<span class="text-[#3a6b45]">---</span>', delay: 220 },
{ html: '<span class="text-[#00ff41] font-bold">&gt; READY — Rendering CV..<span class="ecg-seed-dot" id="ecg-seed-dot">.</span></span>', delay: 220 },
]
interface BootSequenceProps {
onComplete: () => void
}
export function BootSequence({ onComplete }: BootSequenceProps) {
const [isVisible, setIsVisible] = useState(true)
const [lineDelays, setLineDelays] = useState<number[]>([])
useEffect(() => {
const delays: number[] = []
let totalDelay = 0
bootLines.forEach((line) => {
delays.push(totalDelay)
totalDelay += line.delay
})
setLineDelays(delays)
const totalBootTime = totalDelay
const fadeStartTime = totalBootTime + 400
const fadeTimer = setTimeout(() => {
setIsVisible(false)
}, fadeStartTime)
const completeTimer = setTimeout(() => {
onComplete()
}, fadeStartTime + 800)
return () => {
clearTimeout(fadeTimer)
clearTimeout(completeTimer)
}
}, [onComplete])
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="fixed inset-0 z-50 flex flex-col justify-center bg-black p-10 font-mono text-sm overflow-hidden"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
<div className="flex flex-col gap-1 max-w-[640px]">
{bootLines.map((line, index) => (
<motion.div
key={index}
className="whitespace-nowrap leading-relaxed"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: lineDelays[index] / 1000,
duration: 0.4,
ease: 'easeOut',
}}
dangerouslySetInnerHTML={{ __html: line.html }}
/>
))}
<motion.div
className="inline-block w-2 h-4 bg-[#00ff41] ml-1 animate-blink"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: lineDelays[lineDelays.length - 1] / 1000 }}
/>
</div>
</motion.div>
)}
</AnimatePresence>
)
}