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:
@@ -79,11 +79,11 @@ src/
|
|||||||
|
|
||||||
Run `npm create vite@latest . -- --template react-ts` to scaffold the project. Install dependencies: `npm install framer-motion lucide-react`. Initialize Tailwind: `npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p`. Configure `tailwind.config.js` with custom colors (teal #00897B, coral #FF6B6B, etc.). Set up `src/index.css` with Tailwind directives and CSS custom properties matching concept.html.
|
Run `npm create vite@latest . -- --template react-ts` to scaffold the project. Install dependencies: `npm install framer-motion lucide-react`. Initialize Tailwind: `npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p`. Configure `tailwind.config.js` with custom colors (teal #00897B, coral #FF6B6B, etc.). Set up `src/index.css` with Tailwind directives and CSS custom properties matching concept.html.
|
||||||
|
|
||||||
- [ ] **Task 2: Set up project structure and types**
|
- [x] **Task 2: Set up project structure and types**
|
||||||
|
|
||||||
Create the folder structure (`components/`, `hooks/`, `lib/`, `types/`). Define TypeScript interfaces in `types/index.ts` for: `Skill` (name, level, category, color), `Experience` (role, org, date, bullets), `Education` (degree, institution, period, detail), `Project` (title, description, link?). Create `lib/utils.ts` with helper function `calculateSkillOffset(level: number, radius: number): number` that returns `2 * Math.PI * radius * (1 - level / 100)`.
|
Create the folder structure (`components/`, `hooks/`, `lib/`, `types/`). Define TypeScript interfaces in `types/index.ts` for: `Skill` (name, level, category, color), `Experience` (role, org, date, bullets), `Education` (degree, institution, period, detail), `Project` (title, description, link?). Create `lib/utils.ts` with helper function `calculateSkillOffset(level: number, radius: number): number` that returns `2 * Math.PI * radius * (1 - level / 100)`.
|
||||||
|
|
||||||
- [ ] **Task 3: Build BootSequence component**
|
- [x] **Task 3: Build BootSequence component**
|
||||||
|
|
||||||
Create `components/BootSequence.tsx`. Implement terminal typing animation using Framer Motion or CSS transitions. Display boot lines with correct colors (cyan labels, green values, dim text). Use exact boot text from concept.html: "CLINICAL TERMINAL v3.2.1", "Initialising pharmacist profile...", SYSTEM/USER/ROLE/LOCATION, module loading, [OK] lines, READY. Duration: ~4 seconds. Emit `onComplete` callback when finished. Styling: black background, Fira Code font.
|
Create `components/BootSequence.tsx`. Implement terminal typing animation using Framer Motion or CSS transitions. Display boot lines with correct colors (cyan labels, green values, dim text). Use exact boot text from concept.html: "CLINICAL TERMINAL v3.2.1", "Initialising pharmacist profile...", SYSTEM/USER/ROLE/LOCATION, module loading, [OK] lines, READY. Duration: ~4 seconds. Emit `onComplete` callback when finished. Styling: black background, Fira Code font.
|
||||||
|
|
||||||
|
|||||||
@@ -46,3 +46,21 @@
|
|||||||
- **Learnings**:
|
- **Learnings**:
|
||||||
- Need `src/vite-env.d.ts` with `/// <reference types="vite/client" />` for CSS imports
|
- Need `src/vite-env.d.ts` with `/// <reference types="vite/client" />` for CSS imports
|
||||||
- Vite refuses to scaffold in non-empty directory, so manual setup was needed
|
- Vite refuses to scaffold in non-empty directory, so manual setup was needed
|
||||||
|
|
||||||
|
### Iteration 2 — Task 2 & 3: Project structure and BootSequence
|
||||||
|
- **Completed**: Task 2 (Set up project structure and types) - was already done in Task 1
|
||||||
|
- **Completed**: Task 3 - Build BootSequence component
|
||||||
|
- **Files created**:
|
||||||
|
- `src/components/BootSequence.tsx` - Terminal typing animation using Framer Motion
|
||||||
|
- **Design decisions**:
|
||||||
|
- Used Framer Motion's `motion.div` with `initial`/`animate` props for line reveals
|
||||||
|
- Each line animates with opacity 0→1, translateY 8px→0 over 400ms
|
||||||
|
- Staggered delays calculated from cumulative 220ms per line
|
||||||
|
- Blinking cursor implemented with CSS animation class `animate-blink`
|
||||||
|
- Used `AnimatePresence` for smooth exit fade (800ms)
|
||||||
|
- **Boot sequence timing preserved**: 14 lines × 220ms + 400ms pause + 800ms fade = ~4.28s
|
||||||
|
- **Quality checks**: `npm run typecheck` ✓, `npm run build` ✓, `npm run lint` ✓
|
||||||
|
- **Learnings**:
|
||||||
|
- Framer Motion's delay prop uses seconds, not milliseconds
|
||||||
|
- Used `dangerouslySetInnerHTML` for colored spans within boot lines (matches concept.html structure)
|
||||||
|
- CSS classes for blink/seed-pulse animations already existed in index.css from Task 1
|
||||||
|
|||||||
+8
-4
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { Phase } from './types'
|
import type { Phase } from './types'
|
||||||
|
import { BootSequence } from './components/BootSequence'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [phase, setPhase] = useState<Phase>('boot')
|
const [phase, setPhase] = useState<Phase>('boot')
|
||||||
@@ -7,13 +8,16 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{phase === 'boot' && (
|
{phase === 'boot' && (
|
||||||
<div className="fixed inset-0 bg-black flex flex-col justify-center p-10 font-mono text-sm">
|
<BootSequence onComplete={() => setPhase('ecg')} />
|
||||||
<div className="text-ecg-green">CLINICAL TERMINAL v3.2.1</div>
|
)}
|
||||||
|
|
||||||
|
{phase === 'ecg' && (
|
||||||
|
<div className="fixed inset-0 bg-black flex flex-col justify-center items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPhase('content')}
|
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>
|
</button>
|
||||||
</div>
|
</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