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:
+8
-4
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 & 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 & 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">> 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user