portfolio tech
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
const ACCENT = '#22d3ee'
|
||||
const BOX_BG = '#27272a' // zinc-800
|
||||
const BOX_BORDER = '#3f3f46' // zinc-700
|
||||
@@ -30,6 +32,48 @@ function ServiceBox({ x, y, w, h, label, sub }: BoxProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function ClickableServiceBox({ x, y, w, h, label, sub, href }: BoxProps & { href: string }) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
<g
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
filter: hovered ? `drop-shadow(0 0 7px ${ACCENT}90)` : 'none',
|
||||
transition: 'filter 0.2s',
|
||||
}}
|
||||
>
|
||||
<rect
|
||||
x={x} y={y} width={w} height={h} rx={7}
|
||||
fill={BOX_BG}
|
||||
stroke={hovered ? ACCENT : BOX_BORDER}
|
||||
strokeWidth={hovered ? 2 : 1.5}
|
||||
style={{ transition: 'stroke 0.2s, stroke-width 0.2s' }}
|
||||
/>
|
||||
<text
|
||||
x={x + w / 2} y={sub ? y + h / 2 - 5 : 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 + 10}
|
||||
textAnchor="middle" fill={hovered ? ACCENT : TEXT_MUTED} fontSize={10}
|
||||
fontFamily="JetBrains Mono, monospace"
|
||||
style={{ transition: 'fill 0.2s' }}
|
||||
>
|
||||
{sub}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function InfraBox({ x, y, w, h, label, sub }: BoxProps) {
|
||||
return (
|
||||
<g>
|
||||
@@ -90,8 +134,8 @@ export function ArchitectureSection() {
|
||||
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 },
|
||||
{ label: 'rag-view', sub: '/ragview/', x: 140, href: 'https://balexvic.com/ragview/' },
|
||||
{ label: 'analytics-view', sub: '/analytics/', x: 265, href: 'https://balexvic.com/analyticsview/' },
|
||||
]
|
||||
const feCx = (i: number) => feViews[i].x + FE_W / 2
|
||||
|
||||
@@ -256,9 +300,13 @@ export function ArchitectureSection() {
|
||||
stroke={BOX_BORDER} strokeWidth={1} strokeOpacity={0.5} strokeDasharray="3 4" />
|
||||
|
||||
{/* Frontend view boxes */}
|
||||
{feViews.map(fv => (
|
||||
{feViews.map(fv =>
|
||||
fv.href ? (
|
||||
<ClickableServiceBox key={fv.label} x={fv.x} y={Y_SVC} w={FE_W} h={H_MD} label={fv.label} sub={fv.sub} href={fv.href} />
|
||||
) : (
|
||||
<ServiceBox key={fv.label} x={fv.x} y={Y_SVC} w={FE_W} h={H_MD} label={fv.label} sub={fv.sub} />
|
||||
))}
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Backend service boxes */}
|
||||
{beServices.map(bs => (
|
||||
|
||||
@@ -51,9 +51,8 @@ export function ProjectCard({ project, index }: ProjectCardProps) {
|
||||
ref={ref}
|
||||
className={`reveal ${delayClass} group relative flex flex-col bg-zinc-900 border border-zinc-800 rounded-lg p-6 hover:border-zinc-700 hover:bg-zinc-900/80 transition-all duration-300`}
|
||||
>
|
||||
{/* Top: icon + title + demo link */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Top: icon + title */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="text-accent/70 group-hover:text-accent transition-colors">
|
||||
{project.isInfrastructure ? INFRA_ICON : DEFAULT_ICON}
|
||||
</div>
|
||||
@@ -62,27 +61,6 @@ export function ProjectCard({ project, index }: ProjectCardProps) {
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{project.demoUrl && (
|
||||
<a
|
||||
href={project.demoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Open ${project.title} demo`}
|
||||
className="ml-2 shrink-0 text-zinc-600 hover:text-accent transition-colors"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M6 2H2v12h12v-4M9 2h5v5M14 2L7 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-zinc-400 text-sm leading-relaxed mb-5 flex-1 font-body">
|
||||
{project.description}
|
||||
@@ -100,8 +78,34 @@ export function ProjectCard({ project, index }: ProjectCardProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Demo badge */}
|
||||
{/* Action buttons */}
|
||||
{(project.demoUrl || project.docsUrl) && (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{project.demoUrl && (
|
||||
<a
|
||||
href={project.demoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-mono px-3 py-1.5 rounded border border-accent/50 text-accent bg-accent/10 hover:bg-accent/20 hover:border-accent transition-colors"
|
||||
>
|
||||
{project.demoLabel ?? 'Demo'}
|
||||
</a>
|
||||
)}
|
||||
{project.docsUrl && (
|
||||
<a
|
||||
href={project.docsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-mono px-3 py-1.5 rounded border border-zinc-600 text-zinc-400 hover:border-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
API Docs
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Demo badge */}
|
||||
{(project.isLive || project.demoUrl) && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-mono px-1.5 py-0.5 rounded-sm bg-accent/10 text-accent border border-accent/20">
|
||||
<span className="w-1 h-1 rounded-full bg-accent" />
|
||||
|
||||
@@ -8,6 +8,9 @@ const PROJECTS: Project[] = [
|
||||
description:
|
||||
'API Gateway with JWT authentication, OAuth2 (Google, GitHub, Facebook), reactive routing, and per-user rate limiting built on Spring Cloud Gateway.',
|
||||
stack: ['Java 25', 'Spring Boot 3.5', 'Spring Cloud Gateway', 'WebFlux', 'R2DBC', 'PostgreSQL', 'jjwt'],
|
||||
isLive: true,
|
||||
demoUrl: 'https://balexvic.com/ui/dc1/services',
|
||||
demoLabel: 'Consul UI',
|
||||
},
|
||||
{
|
||||
id: 'rag',
|
||||
@@ -15,7 +18,10 @@ const PROJECTS: Project[] = [
|
||||
description:
|
||||
'AI-powered chat with document context. Upload PDF/DOCX, vector semantic search, BM25 reranking, and real-time SSE streaming responses.',
|
||||
stack: ['Java 25', 'Spring Boot 3.5', 'Spring AI', 'pgvector', 'Kafka', 'TikaDocumentReader'],
|
||||
demoUrl: '/ragview/',
|
||||
isLive: true,
|
||||
demoUrl: 'https://balexvic.com/ragview/',
|
||||
docsUrl: 'https://balexvic.com/api/rag/swagger-ui/index.html',
|
||||
demoLabel: 'Open App',
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
@@ -23,15 +29,18 @@ const PROJECTS: Project[] = [
|
||||
description:
|
||||
'Kafka consumer that aggregates platform events into PostgreSQL with a dynamic REST API supporting complex filtering and pagination.',
|
||||
stack: ['Java 25', 'Spring Boot 3.5', 'Kafka', 'JPA', 'Flyway', 'JpaSpecificationExecutor'],
|
||||
demoUrl: '/analyticsview/',
|
||||
isLive: true,
|
||||
demoUrl: 'https://balexvic.com/analyticsview/',
|
||||
demoLabel: 'Open App',
|
||||
},
|
||||
{
|
||||
id: 'audio-foreign',
|
||||
title: 'Audio Foreign',
|
||||
title: 'Learn Deutsch',
|
||||
description:
|
||||
'Voice-based language learning app: speak — get instant AI feedback. Speech-to-text via Whisper, grammar correction via Claude, playback via ElevenLabs.',
|
||||
stack: ['Node.js', 'Express', 'OpenAI Whisper', 'Anthropic Claude API', 'ElevenLabs TTS'],
|
||||
demoUrl: '/deutsch/',
|
||||
demoUrl: 'https://balexvic.com/deutsch/',
|
||||
demoLabel: 'Open App',
|
||||
},
|
||||
{
|
||||
id: 'infra',
|
||||
|
||||
@@ -4,6 +4,9 @@ export interface Project {
|
||||
description: string
|
||||
stack: string[]
|
||||
demoUrl?: string
|
||||
docsUrl?: string
|
||||
demoLabel?: string
|
||||
isLive?: boolean
|
||||
isInfrastructure?: boolean
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user