portfolio tech
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
const ACCENT = '#22d3ee'
|
const ACCENT = '#22d3ee'
|
||||||
const BOX_BG = '#27272a' // zinc-800
|
const BOX_BG = '#27272a' // zinc-800
|
||||||
const BOX_BORDER = '#3f3f46' // zinc-700
|
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) {
|
function InfraBox({ x, y, w, h, label, sub }: BoxProps) {
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
@@ -90,8 +134,8 @@ export function ArchitectureSection() {
|
|||||||
const FE_W = 115
|
const FE_W = 115
|
||||||
const feViews = [
|
const feViews = [
|
||||||
{ label: 'auth-view', sub: '/auth/', x: 15 },
|
{ label: 'auth-view', sub: '/auth/', x: 15 },
|
||||||
{ label: 'rag-view', sub: '/ragview/', x: 140 },
|
{ label: 'rag-view', sub: '/ragview/', x: 140, href: 'https://balexvic.com/ragview/' },
|
||||||
{ label: 'analytics-view', sub: '/analytics/', x: 265 },
|
{ label: 'analytics-view', sub: '/analytics/', x: 265, href: 'https://balexvic.com/analyticsview/' },
|
||||||
]
|
]
|
||||||
const feCx = (i: number) => feViews[i].x + FE_W / 2
|
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" />
|
stroke={BOX_BORDER} strokeWidth={1} strokeOpacity={0.5} strokeDasharray="3 4" />
|
||||||
|
|
||||||
{/* Frontend view boxes */}
|
{/* 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} />
|
<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 */}
|
{/* Backend service boxes */}
|
||||||
{beServices.map(bs => (
|
{beServices.map(bs => (
|
||||||
|
|||||||
@@ -51,9 +51,8 @@ export function ProjectCard({ project, index }: ProjectCardProps) {
|
|||||||
ref={ref}
|
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`}
|
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 */}
|
{/* Top: icon + title */}
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="text-accent/70 group-hover:text-accent transition-colors">
|
<div className="text-accent/70 group-hover:text-accent transition-colors">
|
||||||
{project.isInfrastructure ? INFRA_ICON : DEFAULT_ICON}
|
{project.isInfrastructure ? INFRA_ICON : DEFAULT_ICON}
|
||||||
</div>
|
</div>
|
||||||
@@ -62,27 +61,6 @@ export function ProjectCard({ project, index }: ProjectCardProps) {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</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 */}
|
{/* Description */}
|
||||||
<p className="text-zinc-400 text-sm leading-relaxed mb-5 flex-1 font-body">
|
<p className="text-zinc-400 text-sm leading-relaxed mb-5 flex-1 font-body">
|
||||||
{project.description}
|
{project.description}
|
||||||
@@ -100,8 +78,34 @@ export function ProjectCard({ project, index }: ProjectCardProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Demo badge */}
|
{/* Action buttons */}
|
||||||
|
{(project.demoUrl || project.docsUrl) && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-4">
|
||||||
{project.demoUrl && (
|
{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">
|
<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="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" />
|
<span className="w-1 h-1 rounded-full bg-accent" />
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ const PROJECTS: Project[] = [
|
|||||||
description:
|
description:
|
||||||
'API Gateway with JWT authentication, OAuth2 (Google, GitHub, Facebook), reactive routing, and per-user rate limiting built on Spring Cloud Gateway.',
|
'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'],
|
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',
|
id: 'rag',
|
||||||
@@ -15,7 +18,10 @@ const PROJECTS: Project[] = [
|
|||||||
description:
|
description:
|
||||||
'AI-powered chat with document context. Upload PDF/DOCX, vector semantic search, BM25 reranking, and real-time SSE streaming responses.',
|
'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'],
|
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',
|
id: 'analytics',
|
||||||
@@ -23,15 +29,18 @@ const PROJECTS: Project[] = [
|
|||||||
description:
|
description:
|
||||||
'Kafka consumer that aggregates platform events into PostgreSQL with a dynamic REST API supporting complex filtering and pagination.',
|
'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'],
|
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',
|
id: 'audio-foreign',
|
||||||
title: 'Audio Foreign',
|
title: 'Learn Deutsch',
|
||||||
description:
|
description:
|
||||||
'Voice-based language learning app: speak — get instant AI feedback. Speech-to-text via Whisper, grammar correction via Claude, playback via ElevenLabs.',
|
'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'],
|
stack: ['Node.js', 'Express', 'OpenAI Whisper', 'Anthropic Claude API', 'ElevenLabs TTS'],
|
||||||
demoUrl: '/deutsch/',
|
demoUrl: 'https://balexvic.com/deutsch/',
|
||||||
|
demoLabel: 'Open App',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'infra',
|
id: 'infra',
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export interface Project {
|
|||||||
description: string
|
description: string
|
||||||
stack: string[]
|
stack: string[]
|
||||||
demoUrl?: string
|
demoUrl?: string
|
||||||
|
docsUrl?: string
|
||||||
|
demoLabel?: string
|
||||||
|
isLive?: boolean
|
||||||
isInfrastructure?: boolean
|
isInfrastructure?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user