diff --git a/portfolio-view/src/App.tsx b/portfolio-view/src/App.tsx index 5ffabde..106f2e4 100644 --- a/portfolio-view/src/App.tsx +++ b/portfolio-view/src/App.tsx @@ -4,6 +4,7 @@ import { HeroSection } from './components/hero/HeroSection' import { TechStackSection } from './components/tech/TechStackSection' import { ArchitectureSection } from './components/architecture/ArchitectureSection' import { ProjectsSection } from './components/projects/ProjectsSection' +import { DeliverSection } from './components/deliver/DeliverSection' import { ChatWidget } from './components/chat/ChatWidget' function App() { @@ -15,6 +16,7 @@ function App() { + diff --git a/portfolio-view/src/components/architecture/ArchitectureSection.tsx b/portfolio-view/src/components/architecture/ArchitectureSection.tsx index 68ef4cc..ef6e065 100644 --- a/portfolio-view/src/components/architecture/ArchitectureSection.tsx +++ b/portfolio-view/src/components/architecture/ArchitectureSection.tsx @@ -9,12 +9,20 @@ type BoxProps = { x: number; y: number; w: number; h: number; label: string; sub function ServiceBox({ x, y, w, h, label, sub }: BoxProps) { return ( - - + + {label} {sub && ( - + {sub} )} @@ -25,12 +33,20 @@ function ServiceBox({ x, y, w, h, label, sub }: BoxProps) { function InfraBox({ x, y, w, h, label, sub }: BoxProps) { return ( - - + + {label} {sub && ( - + {sub} )} @@ -39,35 +55,75 @@ function InfraBox({ x, y, w, h, label, sub }: BoxProps) { } export function ArchitectureSection() { - // Layout constants - const W = 760 - const H = 440 + const W = 1000 + const H = 530 - // Gateway - const gw = { x: 280, y: 30, w: 200, h: 52 } - const gwCx = gw.x + gw.w / 2 - const gwCy = gw.y + gw.h + // ── Row Y positions (top edge) ────────────────────────────── + const Y_BROWSER = 18 + const Y_NGINX = 88 + const Y_GATEWAY = 158 + const Y_SVC = 252 + const Y_INFRA = 372 - // Services row — evenly spaced - const svcY = 145 - const svcH = 52 - const svcW = 140 - const services = [ - { label: 'auth-service', sub: 'JWT / OAuth2', x: 20 }, - { label: 'post-service', sub: 'CRUD / feed', x: 185 }, - { label: 'rag-service', sub: 'AI / RAG', x: 350 }, - { label: 'analytics-service', sub: 'metrics', x: 515 }, + const H_SM = 38 // Browser, nginx + const H_MD = 44 // Gateway, services, infra + + const CX = 490 // horizontal centre of the canvas + + // ── Derived bottom edges ──────────────────────────────────── + const browserBot = Y_BROWSER + H_SM + const nginxBot = Y_NGINX + H_SM + const gwBot = Y_GATEWAY + H_MD + const svcBot = Y_SVC + H_MD + const infraMidY = svcBot + (Y_INFRA - svcBot) / 2 // ≈ 332 + + // ── nginx box ─────────────────────────────────────────────── + const nginxW = 150 + const nginxX = CX - nginxW / 2 // 415 + const nginxMidY = Y_NGINX + H_SM / 2 + + // ── Gateway box ───────────────────────────────────────────── + const gwW = 225 + const gwX = CX - gwW / 2 // 378 + const gwRight = gwX + gwW // 603 + + // ── Frontend views (left cluster, row 4) ─────────────────── + const FE_W = 115 + const feViews = [ + { label: 'auth-view', sub: '/auth/', x: 15 }, + { label: 'rag-view', sub: '/ragview/', x: 140 }, + { label: 'analytics-view', sub: '/analytics/', x: 265 }, ] + const feCx = (i: number) => feViews[i].x + FE_W / 2 - // Infra row - const infraY = 310 - const infraH = 52 - const infraW = 160 + // ── Backend services (right cluster, row 4) ───────────────── + const BE_W = 130 + const beServices = [ + { label: 'auth-service', sub: 'JWT / OAuth2', x: 400 }, + { label: 'post-service', sub: 'CRUD / feed', x: 540 }, + { label: 'rag-service', sub: 'AI / RAG', x: 680 }, + { label: 'analytics-service', sub: 'metrics', x: 820 }, + ] + const beCx = (i: number) => beServices[i].x + BE_W / 2 + // beCx: 465, 605, 745, 885 + + // ── Infra row (centred under backend cluster) ─────────────── + // Backend spans x=400 → 950 (width=550) + // 3 × 148 + 2 × 14 = 472 → start = 400 + (550-472)/2 = 439 + const INF_W = 148 const infra = [ - { label: 'PostgreSQL', sub: 'persistence', x: 60 }, - { label: 'Kafka', sub: 'event bus', x: 295 }, - { label: 'Consul', sub: 'service mesh',x: 530 }, + { label: 'PostgreSQL', sub: 'persistence', x: 439 }, // cx = 513 + { label: 'Kafka', sub: 'event bus', x: 601 }, // cx = 675 + { label: 'Consul', sub: 'service mesh', x: 763 }, // cx = 837 ] + const infCx = (i: number) => infra[i].x + INF_W / 2 + // infCx: 513, 675, 837 + + // ── Consul right edge (for the L-path endpoint) ───────────── + const consulRight = infra[2].x + INF_W // 763+148 = 911 + + // Routing rail on the far right (clear of all service boxes, rightmost=950) + const RAIL_X = 972 return ( @@ -91,75 +147,151 @@ export function ArchitectureSection() { aria-label="Microservices architecture diagram" > - + - {/* ── Gateway → Services connections ── */} - {services.map((s) => { - const sx = s.x + svcW / 2 - const sy = svcY - return ( - - ) - })} + {/* ═══════════════════════════════════════════════════════ + SOLID ROUTING ARROWS (request routing) + ═══════════════════════════════════════════════════════ */} - {/* ── Services → Infra connections ── */} - {services.map((s) => { - const sx = s.x + svcW / 2 - const sy = svcY + svcH - // Each service connects to the nearest infra node - const targets = infra.map((inf) => inf.x + infraW / 2) - const nearest = targets.reduce((prev, cur) => - Math.abs(cur - sx) < Math.abs(prev - sx) ? cur : prev - ) - return ( - - ) - })} + {/* Browser → nginx */} + - {/* ── Gateway box ── */} - + {/* nginx → Spring Cloud Gateway */} + - {/* ── Service boxes ── */} - {services.map((s) => ( - + {/* nginx → frontend views (from nginx LEFT side) */} + {feViews.map((fv, i) => ( + ))} - {/* ── Section label: Shared Infrastructure ── */} - + {/* Gateway → backend services */} + {beServices.map((bs, i) => ( + + ))} + + {/* ═══════════════════════════════════════════════════════ + DASHED INFRA DEPENDENCY ARROWS + ═══════════════════════════════════════════════════════ */} + + {/* ── All 4 services → PostgreSQL ── */} + {[0, 1, 2, 3].map(i => ( + + ))} + + {/* ── post-service, rag-service, analytics-service → Kafka ── */} + {[1, 2, 3].map(i => ( + + ))} + + {/* ── All 4 backend services → Consul (faint — many lines) ── */} + {[0, 1, 2, 3].map(i => ( + + ))} + + {/* ── Gateway → Consul (L-path along right rail, avoids all boxes) ── + M gwRight, gwMid → RAIL_X, gwMid → RAIL_X, consulMidY → consulRight, consulMidY */} + + + {/* ═══════════════════════════════════════════════════════ + BOXES (rendered on top of arrows) + ═══════════════════════════════════════════════════════ */} + + {/* Browser */} + + + {/* nginx */} + + + {/* Spring Cloud Gateway */} + + + {/* Cluster labels row 4 */} + + FRONTEND + + + BACKEND SERVICES + + + {/* Subtle divider between clusters */} + + + {/* Frontend view boxes */} + {feViews.map(fv => ( + + ))} + + {/* Backend service boxes */} + {beServices.map(bs => ( + + ))} + + {/* "SHARED INFRASTRUCTURE" label */} + SHARED INFRASTRUCTURE - {/* ── Infra boxes ── */} - {infra.map((inf) => ( - + {/* Infra boxes */} + {infra.map(inf => ( + ))} - {/* ── Legend ── */} - - - request routing - - infra dependency + {/* ═══════════════════════════════════════════════════════ + LEGEND + ═══════════════════════════════════════════════════════ */} + + + + request routing + + + + infra dependency + diff --git a/portfolio-view/src/components/deliver/DeliverSection.tsx b/portfolio-view/src/components/deliver/DeliverSection.tsx new file mode 100644 index 0000000..21adcdd --- /dev/null +++ b/portfolio-view/src/components/deliver/DeliverSection.tsx @@ -0,0 +1,94 @@ +const CARDS = [ + { + title: 'Clean REST APIs', + detail: 'Spring Boot, OpenAPI, tested endpoints', + icon: ( + + + + + ), + }, + { + title: 'Docker-Ready Deployments', + detail: 'Containerized services, Docker Compose', + icon: ( + + + + + + + + + + ), + }, + { + title: 'CI/CD From Day One', + detail: 'GitLab pipelines, automated builds and deploys', + icon: ( + + + + + + + + + + ), + }, + { + title: 'Production Infrastructure', + detail: 'VPS, Nginx, SSL, monitoring', + icon: ( + + + + + + + + + ), + }, +] + +export function DeliverSection() { + return ( + + + + + Deliverables + + + What I Deliver + + + + + {CARDS.map(({ title, detail, icon }) => ( + + + {icon} + + + + {title} + + + {detail} + + + + ))} + + + + ) +} diff --git a/portfolio-view/src/components/hero/HeroSection.tsx b/portfolio-view/src/components/hero/HeroSection.tsx index 6de665f..f7e0830 100644 --- a/portfolio-view/src/components/hero/HeroSection.tsx +++ b/portfolio-view/src/components/hero/HeroSection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' /** * Cycle timeline (all times relative to stage-1 firing): @@ -22,6 +22,7 @@ export function HeroSection() { const [alexOn, setAlexOn] = useState(false) const [shiftLeft, setShiftLeft] = useState(false) const [contentVisible, setContentVisible] = useState(false) + const scrollIndicatorRef = useRef(null) useEffect(() => { const timers: ReturnType[] = [] @@ -51,6 +52,22 @@ export function HeroSection() { return () => { alive = false; timers.forEach(clearTimeout) } }, []) + useEffect(() => { + const el = scrollIndicatorRef.current + if (!el) return + const onScroll = () => { + if (window.scrollY > 80) { + el.style.opacity = '0' + el.style.pointerEvents = 'none' + } else { + el.style.opacity = '1' + el.style.pointerEvents = 'auto' + } + } + window.addEventListener('scroll', onScroll, { passive: true }) + return () => window.removeEventListener('scroll', onScroll) + }, []) + /* ── style helpers ── */ const noTrans = nameStage === 0 @@ -244,18 +261,93 @@ export function HeroSection() { - {/* Scroll hint */} - - - SCROLL - - - - - - + + {/* Animated scroll indicator */} + + + + {/* Label */} + + SCROLL + + + {/* Mouse body */} + + + + + {/* Chevron arrows */} + + {[0, 0.2, 0.4].map((delay, i) => ( + + ))} + + + ) }
+ Deliverables +