Task 1: Initialize React project with Vite, TypeScript, Tailwind
- Scaffolded project with React 18, Vite, TypeScript - Configured Tailwind CSS with custom design tokens (teal, coral, ecg colors) - Added Framer Motion and Lucide React dependencies - Set up Google Fonts (Fira Code, Plus Jakarta Sans, Inter Tight) - Created TypeScript interfaces for CV data types - Added utility function for skill gauge calculations - Quality checks passing: typecheck, build, lint all clean
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react'
|
||||
import type { Phase } from './types'
|
||||
|
||||
function App() {
|
||||
const [phase, setPhase] = useState<Phase>('boot')
|
||||
|
||||
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>
|
||||
<button
|
||||
onClick={() => setPhase('content')}
|
||||
className="mt-8 text-ecg-cyan hover:text-ecg-green transition-colors"
|
||||
>
|
||||
Press to skip boot sequence (placeholder)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === 'content' && (
|
||||
<main className="max-w-[1000px] mx-auto px-8">
|
||||
<section className="min-h-screen flex flex-col justify-center items-center text-center py-20">
|
||||
<h1 className="font-primary font-bold text-5xl text-heading">Andy Charlwood</h1>
|
||||
<p className="text-muted mt-2">Deputy Head of Population Health & Data Analysis</p>
|
||||
<span className="inline-block mt-2 px-4 py-1 border border-teal rounded-full text-xs text-teal font-medium">
|
||||
Norwich, UK
|
||||
</span>
|
||||
<p className="mt-6 max-w-[560px] text-text">
|
||||
GPhC Registered Pharmacist specialising in medicines optimisation, population health analytics, and NHS efficiency programmes.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="py-20">
|
||||
<h2 className="font-primary text-2xl font-bold text-heading text-center mb-8">
|
||||
Components will be built in subsequent tasks
|
||||
</h2>
|
||||
</section>
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,67 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--bg: #FFFFFF;
|
||||
--text: #334155;
|
||||
--heading: #0F172A;
|
||||
--teal: #00897B;
|
||||
--teal-light: rgba(0, 137, 123, 0.08);
|
||||
--teal-medium: rgba(0, 137, 123, 0.15);
|
||||
--coral: #FF6B6B;
|
||||
--coral-light: rgba(255, 107, 107, 0.08);
|
||||
--muted: #94A3B8;
|
||||
--border: #E2E8F0;
|
||||
--card-bg: #FFFFFF;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
|
||||
--radius: 16px;
|
||||
--font-primary: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-secondary: 'Inter Tight', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-primary);
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.font-primary {
|
||||
font-family: var(--font-primary);
|
||||
}
|
||||
.font-secondary {
|
||||
font-family: var(--font-secondary);
|
||||
}
|
||||
.font-mono {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes seedPulse {
|
||||
0%, 100% { text-shadow: 0 0 8px #00ff41, 0 0 16px #00ff41; }
|
||||
50% { text-shadow: 0 0 14px #00ff41, 0 0 28px #00ff41, 0 0 40px rgba(0,255,65,0.3); }
|
||||
}
|
||||
|
||||
.animate-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
.animate-seed-pulse {
|
||||
animation: seedPulse 0.6s ease-in-out infinite;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function calculateSkillOffset(level: number, radius: number): number {
|
||||
const circumference = 2 * Math.PI * radius
|
||||
return circumference * (1 - level / 100)
|
||||
}
|
||||
|
||||
export function formatBootLine(text: string): string {
|
||||
return text
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
export interface Skill {
|
||||
name: string
|
||||
level: number
|
||||
category: 'Technical' | 'Clinical' | 'Strategic'
|
||||
color: 'teal' | 'coral'
|
||||
}
|
||||
|
||||
export interface Experience {
|
||||
role: string
|
||||
org: string
|
||||
date: string
|
||||
bullets: string[]
|
||||
isCurrent?: boolean
|
||||
}
|
||||
|
||||
export interface Education {
|
||||
degree: string
|
||||
institution: string
|
||||
period: string
|
||||
detail: string
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
title: string
|
||||
description: string
|
||||
link?: string
|
||||
}
|
||||
|
||||
export interface ContactItem {
|
||||
icon: 'phone' | 'mail' | 'linkedin' | 'mapPin'
|
||||
value: string
|
||||
label: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
export type Phase = 'boot' | 'ecg' | 'content'
|
||||
|
||||
export interface BootLine {
|
||||
html: string
|
||||
delay: number
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user