portfolio tech
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { Header } from './components/layout/Header'
|
import { Header } from './components/layout/Header'
|
||||||
import { Footer } from './components/layout/Footer'
|
import { Footer } from './components/layout/Footer'
|
||||||
import { HeroSection } from './components/hero/HeroSection'
|
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 { ProjectsSection } from './components/projects/ProjectsSection'
|
||||||
import { ChatWidget } from './components/chat/ChatWidget'
|
import { ChatWidget } from './components/chat/ChatWidget'
|
||||||
|
|
||||||
@@ -10,6 +12,8 @@ function App() {
|
|||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
<TechStackSection />
|
||||||
|
<ArchitectureSection />
|
||||||
<ProjectsSection />
|
<ProjectsSection />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
const ACCENT = '#22d3ee'
|
||||||
|
const BOX_BG = '#27272a' // zinc-800
|
||||||
|
const BOX_BORDER = '#3f3f46' // zinc-700
|
||||||
|
const TEXT = '#fafafa'
|
||||||
|
const TEXT_MUTED = '#a1a1aa'
|
||||||
|
|
||||||
|
type BoxProps = { x: number; y: number; w: number; h: number; label: string; sub?: string }
|
||||||
|
|
||||||
|
function ServiceBox({ x, y, w, h, label, sub }: BoxProps) {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<rect x={x} y={y} width={w} height={h} rx={8} fill={BOX_BG} stroke={BOX_BORDER} strokeWidth={1.5} />
|
||||||
|
<text x={x + w / 2} y={sub ? y + h / 2 - 6 : y + h / 2 + 5} textAnchor="middle" fill={TEXT} fontSize={13} fontFamily="JetBrains Mono, monospace" fontWeight={600}>
|
||||||
|
{label}
|
||||||
|
</text>
|
||||||
|
{sub && (
|
||||||
|
<text x={x + w / 2} y={y + h / 2 + 12} textAnchor="middle" fill={TEXT_MUTED} fontSize={10} fontFamily="JetBrains Mono, monospace">
|
||||||
|
{sub}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfraBox({ x, y, w, h, label, sub }: BoxProps) {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<rect x={x} y={y} width={w} height={h} rx={8} fill={BOX_BG} stroke={ACCENT} strokeWidth={1} strokeDasharray="4 3" opacity={0.85} />
|
||||||
|
<text x={x + w / 2} y={sub ? y + h / 2 - 6 : y + h / 2 + 5} textAnchor="middle" fill={TEXT} fontSize={12} fontFamily="JetBrains Mono, monospace" fontWeight={600}>
|
||||||
|
{label}
|
||||||
|
</text>
|
||||||
|
{sub && (
|
||||||
|
<text x={x + w / 2} y={y + h / 2 + 12} textAnchor="middle" fill={TEXT_MUTED} fontSize={10} fontFamily="JetBrains Mono, monospace">
|
||||||
|
{sub}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchitectureSection() {
|
||||||
|
// Layout constants
|
||||||
|
const W = 760
|
||||||
|
const H = 440
|
||||||
|
|
||||||
|
// Gateway
|
||||||
|
const gw = { x: 280, y: 30, w: 200, h: 52 }
|
||||||
|
const gwCx = gw.x + gw.w / 2
|
||||||
|
const gwCy = gw.y + gw.h
|
||||||
|
|
||||||
|
// 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 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Infra row
|
||||||
|
const infraY = 310
|
||||||
|
const infraH = 52
|
||||||
|
const infraW = 160
|
||||||
|
const infra = [
|
||||||
|
{ label: 'PostgreSQL', sub: 'persistence', x: 60 },
|
||||||
|
{ label: 'Kafka', sub: 'event bus', x: 295 },
|
||||||
|
{ label: 'Consul', sub: 'service mesh',x: 530 },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="architecture" className="py-24 px-6">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-14">
|
||||||
|
<p className="font-mono text-xs text-accent tracking-widest mb-3 uppercase">
|
||||||
|
System Design
|
||||||
|
</p>
|
||||||
|
<h2 className="font-heading font-bold text-4xl md:text-5xl text-white tracking-tight">
|
||||||
|
Platform Architecture
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diagram */}
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900/60 p-4 overflow-x-auto">
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${W} ${H}`}
|
||||||
|
width="100%"
|
||||||
|
style={{ maxWidth: W, display: 'block', margin: '0 auto' }}
|
||||||
|
aria-label="Microservices architecture diagram"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||||||
|
<path d="M0,0 L0,6 L8,3 z" fill={ACCENT} />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* ── Gateway → Services connections ── */}
|
||||||
|
{services.map((s) => {
|
||||||
|
const sx = s.x + svcW / 2
|
||||||
|
const sy = svcY
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={s.label}
|
||||||
|
x1={gwCx} y1={gwCy}
|
||||||
|
x2={sx} y2={sy - 2}
|
||||||
|
stroke={ACCENT}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeOpacity={0.7}
|
||||||
|
markerEnd="url(#arrow)"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* ── 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 (
|
||||||
|
<line
|
||||||
|
key={s.label + '-infra'}
|
||||||
|
x1={sx} y1={sy}
|
||||||
|
x2={nearest} y2={infraY - 2}
|
||||||
|
stroke={ACCENT}
|
||||||
|
strokeWidth={1}
|
||||||
|
strokeOpacity={0.35}
|
||||||
|
strokeDasharray="5 4"
|
||||||
|
markerEnd="url(#arrow)"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* ── Gateway box ── */}
|
||||||
|
<ServiceBox {...gw} label="Spring Cloud Gateway" />
|
||||||
|
|
||||||
|
{/* ── Service boxes ── */}
|
||||||
|
{services.map((s) => (
|
||||||
|
<ServiceBox key={s.label} x={s.x} y={svcY} w={svcW} h={svcH} label={s.label} sub={s.sub} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ── Section label: Shared Infrastructure ── */}
|
||||||
|
<text x={W / 2} y={infraY - 18} textAnchor="middle" fill={TEXT_MUTED} fontSize={10} fontFamily="JetBrains Mono, monospace" letterSpacing={2}>
|
||||||
|
SHARED INFRASTRUCTURE
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* ── Infra boxes ── */}
|
||||||
|
{infra.map((inf) => (
|
||||||
|
<InfraBox key={inf.label} x={inf.x} y={infraY} w={infraW} h={infraH} label={inf.label} sub={inf.sub} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ── Legend ── */}
|
||||||
|
<g transform={`translate(${W - 175}, ${H - 52})`}>
|
||||||
|
<line x1={0} y1={8} x2={28} y2={8} stroke={ACCENT} strokeWidth={1.5} markerEnd="url(#arrow)" />
|
||||||
|
<text x={34} y={12} fill={TEXT_MUTED} fontSize={10} fontFamily="JetBrains Mono, monospace">request routing</text>
|
||||||
|
<line x1={0} y1={26} x2={28} y2={26} stroke={ACCENT} strokeWidth={1} strokeDasharray="5 4" markerEnd="url(#arrow)" />
|
||||||
|
<text x={34} y={30} fill={TEXT_MUTED} fontSize={10} fontFamily="JetBrains Mono, monospace">infra dependency</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
164
portfolio-view/src/components/tech/TechStackSection.tsx
Normal file
164
portfolio-view/src/components/tech/TechStackSection.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
const TECH = [
|
||||||
|
{
|
||||||
|
label: 'Java',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<path d="M20.2 31.6s-1.8 1 1.3 1.4c3.7.4 5.6.4 9.7-.4 0 0 1.1.7 2.6 1.2-9.2 3.9-20.8-.2-13.6-2.2z" fill="#5382a1"/>
|
||||||
|
<path d="M19 28.7s-2 1.5 1.1 1.8c4 .4 7.2.5 12.7-.6 0 0 .7.8 1.9 1.2-11.2 3.3-23.7.3-15.7-2.4z" fill="#5382a1"/>
|
||||||
|
<path d="M26.3 20.1c2.3 2.6-.6 5-.6 5s5.8-3 3.1-6.7c-2.5-3.5-4.4-5.2 5.9-11.2 0 0-16.1 4-8.4 12.9z" fill="#e76f00"/>
|
||||||
|
<path d="M34.8 34.4s1.3 1.1-1.5 2c-5.3 1.6-22 2.1-26.7.1-1.7-.7 1.5-1.8 2.5-2 1-.2 1.6-.2 1.6-.2-1.8-1.3-11.9 2.5-5.1 3.6 18.5 3 33.7-1.3 29.2-3.5z" fill="#5382a1"/>
|
||||||
|
<path d="M21 24.4s-8.4 2-3 2.7c2.3.3 6.9.2 11.1-.1 3.5-.3 7-.9 7-.9s-1.2.5-2.1 1.1c-8.5 2.2-24.9 1.2-20.2-.9 4-1.9 7.2-1.9 7.2-1.9z" fill="#5382a1"/>
|
||||||
|
<path d="M30.9 29.9c8.6-4.5 4.6-8.8 1.8-8.2-.7.1-1 .3-1 .3s.3-.4.8-.6c5.7-2 10.1 5.9-1.8 9-.1 0 .1-.1.2-.5z" fill="#5382a1"/>
|
||||||
|
<path d="M23.7 38.4s1 .9-1.1 1.5c-4 1.2-16.7 1.6-20.2.1-1.3-.6 1.1-1.4 1.9-1.5.8-.2 1.2-.1 1.2-.1-1.4-1-9 2-3.9 2.8 14 2.3 25.5-1 22.1-2.8z" fill="#5382a1"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Spring Boot',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<path d="M43.8 9.7a21.8 21.8 0 0 1-2.5 4.5A22 22 0 0 0 24 6C13 6 4 15 4 26s9 20 20 20 20-9 20-20c0-4.6-1.6-8.9-4.2-12.3z" fill="#6db33f"/>
|
||||||
|
<path d="M40.7 7.5c-.3 1.6-1.1 3-2.2 4.1a3 3 0 0 1-4.2 0 3 3 0 0 1 0-4.2c1-.9 2.4-1.7 4-2 .8-.1 1.7 0 2.4.2.2.6.2 1.3 0 1.9z" fill="#6db33f"/>
|
||||||
|
<path d="M24 13c-7.2 0-13 5.8-13 13s5.8 13 13 13 13-5.8 13-13-5.8-13-13-13zm0 21a8 8 0 1 1 0-16 8 8 0 0 1 0 16z" fill="white"/>
|
||||||
|
<circle cx="24" cy="26" r="4" fill="#6db33f"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'React',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<ellipse cx="24" cy="24" rx="4" ry="4" fill="#61dafb"/>
|
||||||
|
<ellipse cx="24" cy="24" rx="20" ry="7.5" stroke="#61dafb" strokeWidth="2" fill="none"/>
|
||||||
|
<ellipse cx="24" cy="24" rx="20" ry="7.5" stroke="#61dafb" strokeWidth="2" fill="none" transform="rotate(60 24 24)"/>
|
||||||
|
<ellipse cx="24" cy="24" rx="20" ry="7.5" stroke="#61dafb" strokeWidth="2" fill="none" transform="rotate(120 24 24)"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TypeScript',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<rect x="4" y="4" width="40" height="40" rx="4" fill="#3178c6"/>
|
||||||
|
<path d="M22 20h-6v-3h15v3h-6v14h-3V20z" fill="white"/>
|
||||||
|
<path d="M29 30.5c0 1 .8 1.8 2.3 1.8 1.3 0 2-.6 2-1.5 0-.9-.6-1.4-2-1.8l-.9-.3c-2.1-.6-3.1-1.6-3.1-3.2 0-2 1.7-3.3 4-3.3 2.2 0 3.8 1.2 3.8 3.1h-2.4c0-.9-.6-1.5-1.7-1.5-1 0-1.6.5-1.6 1.3 0 .8.5 1.2 1.8 1.6l.8.2c2.4.7 3.4 1.7 3.4 3.4 0 2.2-1.7 3.5-4.3 3.5-2.5 0-4.1-1.3-4.1-3.3H29z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PostgreSQL',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<ellipse cx="24" cy="14" rx="14" ry="8" fill="#336791"/>
|
||||||
|
<path d="M10 14v16c0 4.4 6.3 8 14 8s14-3.6 14-8V14" stroke="#336791" strokeWidth="2" fill="none"/>
|
||||||
|
<ellipse cx="24" cy="14" rx="14" ry="8" fill="#336791"/>
|
||||||
|
<path d="M10 22c0 4.4 6.3 8 14 8s14-3.6 14-8" stroke="#4a7fb5" strokeWidth="1.5" fill="none"/>
|
||||||
|
<path d="M10 18c0 4.4 6.3 8 14 8s14-3.6 14-8" stroke="#4a7fb5" strokeWidth="1.5" fill="none"/>
|
||||||
|
<path d="M34 10c3 1.5 4 3 4 4" stroke="#6ca0c8" strokeWidth="1.5" strokeLinecap="round"/>
|
||||||
|
<circle cx="35" cy="8" r="3" fill="#4a7fb5"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Docker',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<rect x="6" y="18" width="6" height="5" rx="1" fill="#2496ed"/>
|
||||||
|
<rect x="13" y="18" width="6" height="5" rx="1" fill="#2496ed"/>
|
||||||
|
<rect x="20" y="18" width="6" height="5" rx="1" fill="#2496ed"/>
|
||||||
|
<rect x="20" y="12" width="6" height="5" rx="1" fill="#2496ed"/>
|
||||||
|
<rect x="27" y="18" width="6" height="5" rx="1" fill="#2496ed"/>
|
||||||
|
<rect x="13" y="12" width="6" height="5" rx="1" fill="#2496ed"/>
|
||||||
|
<path d="M42 22c-1-2-4-2.5-5.5-2.5-.3-2.5-2-4.5-4-4.5h-1c.5 1 .5 2 .5 3H33v2h-6v-2h-1V16h-6v2h-1v-4h-6v4H7C5 18 4 19 4 21c0 5 3.5 9 8.5 9h18c4 0 7.5-2.5 8.5-6 1.5 0 2.5 0 3-2z" fill="#2496ed"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kafka',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<circle cx="24" cy="10" r="5" fill="#231f20" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<circle cx="10" cy="34" r="5" fill="#231f20" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<circle cx="38" cy="34" r="5" fill="#231f20" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<line x1="24" y1="15" x2="14" y2="30" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<line x1="24" y1="15" x2="34" y2="30" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<circle cx="24" cy="24" r="4" fill="#231f20" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<line x1="24" y1="28" x2="14" y2="30" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
<line x1="24" y1="28" x2="34" y2="30" stroke="#ffffff" strokeWidth="1.5"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nginx',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<polygon points="24,4 42,14 42,34 24,44 6,34 6,14" fill="#009900"/>
|
||||||
|
<path d="M16 14h4l8 14V14h4v20h-4L20 20v14h-4V14z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'GitLab CI/CD',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<path d="M24 43L12 23l-8 0L24 5l20 18h-8L24 43z" fill="#e24329"/>
|
||||||
|
<path d="M24 43L12 23H4l8-10 12 30z" fill="#fc6d26"/>
|
||||||
|
<path d="M24 43L36 23l8 0L36 13 24 43z" fill="#fc6d26"/>
|
||||||
|
<path d="M4 23L12 13l0 10H4z" fill="#fca326"/>
|
||||||
|
<path d="M44 23L36 13l0 10H44z" fill="#fca326"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Linux',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-10 h-10">
|
||||||
|
<ellipse cx="24" cy="18" rx="9" ry="12" fill="#f5c518"/>
|
||||||
|
<circle cx="20" cy="16" r="2" fill="#333"/>
|
||||||
|
<circle cx="28" cy="16" r="2" fill="#333"/>
|
||||||
|
<circle cx="20.5" cy="16.5" r=".7" fill="white"/>
|
||||||
|
<circle cx="28.5" cy="16.5" r=".7" fill="white"/>
|
||||||
|
<path d="M20 22c1.3 1.5 6.7 1.5 8 0" stroke="#333" strokeWidth="1.2" strokeLinecap="round" fill="none"/>
|
||||||
|
<path d="M15 30c0-5 4-9 9-9s9 4 9 9v2l2 4-2 1-2-3h-14l-2 3-2-1 2-4v-2z" fill="#f5c518"/>
|
||||||
|
<path d="M17 36l-4 4 2 2 4-3M31 36l4 4-2 2-4-3" stroke="#333" strokeWidth="1.2" strokeLinecap="round" fill="none"/>
|
||||||
|
<ellipse cx="24" cy="18" rx="4" ry="2" fill="#e6b800"/>
|
||||||
|
<path d="M15 22c-1 2-1 5 0 6M33 22c1 2 1 5 0 6" stroke="#ddb800" strokeWidth="1.2" strokeLinecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function TechStackSection() {
|
||||||
|
return (
|
||||||
|
<section id="tech-stack" className="py-24 px-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Section header */}
|
||||||
|
<div className="mb-14">
|
||||||
|
<p className="font-mono text-xs text-accent tracking-widest mb-3 uppercase">
|
||||||
|
Stack
|
||||||
|
</p>
|
||||||
|
<h2 className="font-heading font-bold text-4xl md:text-5xl text-white tracking-tight">
|
||||||
|
Tech Stack
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid: 3 cols mobile → 5 cols desktop */}
|
||||||
|
<div className="grid grid-cols-3 md:grid-cols-5 gap-4">
|
||||||
|
{TECH.map(({ label, icon }) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex flex-col items-center justify-center gap-3 p-5 bg-zinc-900 border border-zinc-800 rounded-lg hover:border-zinc-600 hover:bg-zinc-800/70 transition-all duration-200 group"
|
||||||
|
>
|
||||||
|
<div className="opacity-90 group-hover:opacity-100 transition-opacity duration-200">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-xs text-zinc-400 group-hover:text-zinc-200 transition-colors duration-200 text-center leading-tight">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user