feat: portfolio redesign with i18n, RBC project showcase, CI/CD

- Add i18n support (EN/ZH) with language toggle
- Redesign header with education dropdown, contact copy buttons
- Add RBC project showcase with bullet points, skills highlighting
- Create terraform-drift placeholder page
- Add ProjectShowcase component with media toggle (image/video)
- Update experience timeline with action-driven descriptions
- Add Docker + Forgejo CI workflow for image builds
- Update K8s manifests for riotpiao.com ingress
- Add DESIGN.md documenting design system patterns
This commit is contained in:
Story Crater Bot
2026-08-31 13:59:46 -07:00
parent e404dae7c8
commit b9ae470bc1
26 changed files with 1724 additions and 250 deletions
+15
View File
@@ -0,0 +1,15 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.env.*.local
.next
.DS_Store
*.log
infra
.claude
.vscode
.idea
+58
View File
@@ -0,0 +1,58 @@
name: Build & Push Portfolio Image
on:
push:
branches:
- main
paths:
- 'app/**'
- 'components/**'
- 'lib/**'
- 'public/**'
- 'styles/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'next.config.js'
- 'tsconfig.json'
- 'Dockerfile'
- '.dockerignore'
jobs:
build-push:
runs-on: node
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Forgejo Registry
uses: docker/login-action@v3
with:
registry: forgejo.riotpiao.com
username: ${{ secrets.FORGEJO_REGISTRY_USER }}
password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Extract metadata
id: meta
run: |
echo "timestamp=$(date -u +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
forgejo.riotpiao.com/rock/portfolio:latest
forgejo.riotpiao.com/rock/portfolio:${{ steps.meta.outputs.sha_short }}
platforms: linux/amd64
cache-from: type=registry,ref=forgejo.riotpiao.com/rock/portfolio:buildcache
cache-to: type=registry,ref=forgejo.riotpiao.com/rock/portfolio:buildcache,mode=max
- name: Verify image pushed
run: |
echo "✓ Image pushed to forgejo.riotpiao.com/rock/portfolio:latest"
echo "✓ Image tagged with SHA: ${{ steps.meta.outputs.sha_short }}"
+341
View File
@@ -0,0 +1,341 @@
# Portfolio Design System
## Color Palette
### Primary Colors
- **Blue**: `blue-600` (primary actions, links, accents)
- **Purple**: `purple-600` (gradients, secondary accents)
- **Green**: `green-500` (live status, success)
### Neutral Colors
- **Light Mode**:
- Background: `white`, `gray-50`
- Text Primary: `gray-900`
- Text Secondary: `gray-600`, `gray-700`
- Borders: `gray-200`, `gray-300`
- **Dark Mode**:
- Background: `gray-950`, `gray-900`
- Text Primary: `white`
- Text Secondary: `gray-400`, `gray-300`
- Borders: `gray-800`, `gray-700`
### Status Colors
- Live: `green-100/900` bg, `green-800/200` text
- Building: `blue-100/900` bg, `blue-800/200` text
- Planning: `gray-100/800` bg, `gray-800/200` text
### Timeline Colors (gradients)
- riotpiao.com: `from-green-500 to-green-600`
- RBC: `from-red-500 to-red-600`
- AWS: `from-orange-500 to-orange-600`
- Titus: `from-cyan-500 to-cyan-600`
- NAV Canada: `from-blue-500 to-blue-600`
---
## Typography
### Font Sizes
- Hero Title: `text-4xl md:text-5xl font-bold`
- Section Title: `text-4xl font-bold`
- Card Title: `text-2xl font-bold` or `text-xl font-bold`
- Subtitle: `text-xl font-medium` or `text-lg`
- Body: `text-sm` or `text-base`
- Caption: `text-xs`
- Mono/Code: `font-mono text-xs`
### Font Weights
- Bold: `font-bold` (titles, emphasis)
- Semibold: `font-semibold` (subtitles, labels)
- Medium: `font-medium` (secondary text)
---
## Spacing
### Layout
- Max Width: `max-w-6xl` (main container), `max-w-4xl` (content), `max-w-3xl` (cards), `max-w-5xl` (hero image)
- Padding: `px-6` (horizontal), `py-20` or `py-24` (section vertical)
- Gap: `gap-8` (cards), `gap-6` (nav items), `gap-4` (buttons), `gap-2` (tags)
### Component Spacing
- Section margin bottom: `mb-16`
- Card padding: `p-6`
- Button padding: `px-8 py-3` (large), `px-4 py-2` (medium), `px-3 py-1` (small)
---
## Components
### Buttons
**Primary Button**
```jsx
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
```
**Secondary/Outline Button**
```jsx
className="border-2 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 px-4 py-2 rounded-lg font-semibold transition"
```
**Language Toggle**
```jsx
className="px-3 py-1 text-sm font-semibold rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800"
```
**Icon Button**
```jsx
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
```
### Cards
**Standard Card**
```jsx
className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-6 hover:shadow-lg transition-shadow"
```
**Project Showcase Card**
```jsx
className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow"
```
### Tags/Pills
**Skill Tag**
```jsx
className="text-xs px-3 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300"
```
**Status Badge**
```jsx
// Live
className="px-3 py-1 rounded-full text-xs font-semibold bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
// Building
className="px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
```
### Dropdowns
**Dropdown Container**
```jsx
className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg p-4 space-y-3"
```
### Modals
**Modal Overlay**
```jsx
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
```
**Modal Content**
```jsx
className="bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-2xl w-full max-h-96 overflow-y-auto"
```
**Modal Header (Gradient)**
```jsx
className="sticky top-0 bg-gradient-to-r from-blue-600 to-purple-600 px-6 py-6 flex justify-between items-center"
```
---
## Animations (Framer Motion)
### Fade In Up
```jsx
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
```
### Scale In
```jsx
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
```
### Stagger Children
```jsx
variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.15 },
},
}}
```
### Role Text Rotation
```jsx
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
transition={{ duration: 0.6, ease: 'easeInOut' }}
```
### Background Pulse
```jsx
animate={{
scale: [1, 1.15, 1],
opacity: [0.25, 0.4, 0.25],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: 'easeInOut',
}}
```
---
## Gradients
### Hero Background
```jsx
className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-950 dark:to-gray-900"
```
### Timeline Line
```jsx
className="bg-gradient-to-b from-blue-500 via-purple-500 to-green-500"
```
### Stat Highlight
```jsx
className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800"
```
### Modal Header
```jsx
className="bg-gradient-to-r from-blue-600 to-purple-600"
```
---
## Z-Index Layers
- `z-0`: Background images
- `z-10`: Content overlays (avatar, text)
- `z-20`: Avatar + rotating text group
- `z-40`: Terminal button
- `z-50`: Header, Modals
---
## Responsive Breakpoints
- Mobile: Default (no prefix)
- Tablet/Desktop: `md:` (768px+)
- Large Desktop: `lg:` (1024px+)
### Common Patterns
```jsx
// Hide on mobile, show on desktop
className="hidden md:flex"
// Show on mobile, hide on desktop
className="md:hidden"
// Responsive grid
className="grid grid-cols-1 md:grid-cols-2 gap-8"
// Responsive text
className="text-4xl md:text-5xl"
// Responsive padding
className="px-4 md:px-6"
```
---
## Icons (Lucide React)
### Sizes
- Navigation: `size={20}`
- Buttons: `size={16}`
- Cards: `size={24}`
- Hero: `size={32}`
- Close: `size={24}`
### Common Icons
- `Menu`, `X` - Mobile nav
- `Moon`, `Sun` - Theme toggle
- `ExternalLink` - External links
- `Network`, `Shield`, `Code`, `Sparkles`, `Zap`, `Cpu` - Role icons
- `GitBranch`, `Database`, `MessageSquare`, `Layers`, `BarChart3` - Feature icons
---
## Internationalization (i18n)
### Structure
```
lib/
translations.json # All translations
LanguageContext.tsx # React context provider
```
### Usage
```jsx
import { useLanguage } from '@/lib/LanguageContext'
const { lang, setLang, t } = useLanguage()
// Access translations
<h1>{t.hero.title}</h1>
```
### Language Toggle Button
Shows opposite language:
- When `en`: Shows `中文`
- When `zh`: Shows `EN`
---
## File Structure
```
components/
Header.tsx # Navigation, dropdowns, language toggle
HeroBlobFlow.tsx # Hero with avatar, rotating roles
ExperienceTimeline.tsx # Work history timeline
ProjectShowcase.tsx # Project cards
InteractiveTerminal.tsx # Cmd+K terminal
TypewriterText.tsx # Typewriter effect
lib/
TerminalContext.tsx # Terminal open/close state
LanguageContext.tsx # Language state + translations
translations.json # EN/ZH translations
app/
page.tsx # Main page
layout.tsx # Root layout with providers
globals.css # Tailwind + global styles
public/
avatar.jpeg # Profile photo
hire-me.png # Dark mode hero background
hire-me-light.png # Light mode hero background
```
---
## Dark Mode
Implemented via Tailwind's `dark:` prefix. Toggle class on `<html>` element.
```jsx
// Toggle
document.documentElement.classList.toggle('dark')
// Check
document.documentElement.classList.contains('dark')
```
Observe changes via MutationObserver for components that need to react to theme changes (e.g., background image swap).
+45
View File
@@ -0,0 +1,45 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY . .
# Build Next.js app
RUN pnpm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Copy built app from builder
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV NODE_ENV=production
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
+6
View File
@@ -1,5 +1,7 @@
import './globals.css'
import Header from '@/components/Header'
import { TerminalProvider } from '@/lib/TerminalContext'
import { LanguageProvider } from '@/lib/LanguageContext'
export const metadata = {
title: 'Rock Liang — Portfolio & Live Infrastructure',
@@ -14,8 +16,12 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body className="bg-white dark:bg-gray-950 text-gray-900 dark:text-white">
<LanguageProvider>
<TerminalProvider>
<Header />
{children}
</TerminalProvider>
</LanguageProvider>
</body>
</html>
);
+112 -86
View File
@@ -1,71 +1,35 @@
'use client'
import { motion } from 'framer-motion'
import { FeatureCard } from '@/components/FeatureCard'
import { useState } from 'react'
import { X } from 'lucide-react'
import { ProjectShowcase } from '@/components/ProjectShowcase'
import { HeroBlobFlow } from '@/components/HeroBlobFlow'
import { InteractiveTerminal } from '@/components/InteractiveTerminal'
import { ExperienceTimeline } from '@/components/ExperienceTimeline'
import {
GitBranch,
Database,
BarChart3,
MessageSquare,
Cpu,
Layers,
} from 'lucide-react'
const features = [
{
icon: GitBranch,
title: 'Infrastructure Platform',
description: 'Multi-region Terraform + Argo CD GitOps on Kubernetes',
href: '/infrastructure',
stat: '40% CPU reduction',
status: 'live' as const,
},
{
icon: Cpu,
title: 'Distributed Systems',
description: 'gRPC, Kafka, AWS Step Functions across 57+ regions',
href: '/systems',
stat: 'Mission-critical',
status: 'live' as const,
},
{
icon: MessageSquare,
title: 'LLM Systems',
description: 'CPU-bound inference optimization, INT4/INT8 quantization',
href: '/llm',
stat: '60% latency cut',
status: 'live' as const,
},
{
icon: Database,
title: 'Kafka Cluster',
description: 'Strimzi KRaft cluster with auto-scaling brokers',
href: '/kafka',
stat: '3 brokers',
status: 'live' as const,
},
{
icon: Layers,
title: 'Open Source',
description: 'go-flink: distributed DataLakeHouse in Go',
href: '/opensource',
stat: 'Public repo',
status: 'live' as const,
},
{
icon: BarChart3,
title: 'Observability',
description: 'Prometheus, Grafana, Dynatrace — live metrics',
href: '/infrastructure',
stat: '99.2% uptime',
status: 'live' as const,
},
]
import { useLanguage } from '@/lib/LanguageContext'
export default function Home() {
const [bioModalOpen, setBioModalOpen] = useState(false)
const { t } = useLanguage()
const projects = t.projects.items.map((item, index) => ({
...item,
id: index === 1 ? 'project-rbc' : undefined,
status: index === 1
? 'completed' as const
: index === 5
? 'building' as const
: 'live' as const,
media: index === 0
? { type: 'video' as const, url: 'https://www.youtube.com/watch?v=wdJ5DN15jus' }
: index === 1
? { type: 'image' as const, url: '/rbc-images.jpeg' }
: undefined,
videoUrl: index === 5 ? 'https://github.com/rockliang/go-flink' : '#',
articleUrl: index === 0 ? 'https://lnkd.in/p/gm2PZkWw' : '#',
}))
return (
<main className="min-h-screen">
{/* Hero */}
@@ -82,30 +46,22 @@ export default function Home() {
className="text-center mt-16"
>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-2">
Full-Stack Systems Engineer
{t.hero.title}
</h1>
<p className="text-xl text-gray-700 dark:text-gray-300 mb-6 font-medium">
Infrastructure × Backend × LLM Systems
{t.hero.subtitle}
</p>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-10">
Building observable, fault-tolerant systems from bare metal to cloud. Optimizing cost (infrastructure) × performance (inference) × reliability (SRE).
{t.hero.description}
</p>
<div className="flex gap-4 justify-center flex-wrap mb-8">
<a
href="#features"
<button
onClick={() => setBioModalOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
>
Explore Systems
</a>
<a
href="https://github.com/rockliang"
target="_blank"
rel="noopener noreferrer"
className="border-2 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 transition"
>
GitHub
</a>
{t.hero.exploreMore}
</button>
</div>
<motion.div
@@ -114,7 +70,7 @@ export default function Home() {
transition={{ duration: 0.8, delay: 1.2 }}
className="text-sm text-gray-600 dark:text-gray-400"
>
💡 Press <kbd className="bg-gray-200 dark:bg-gray-800 px-2 py-1 rounded">Cmd+K</kbd> to explore via terminal
💡 {t.hero.terminalHint} <kbd className="bg-gray-200 dark:bg-gray-800 px-2 py-1 rounded">Cmd+K</kbd> {t.hero.terminalHintSuffix}
</motion.div>
</motion.div>
</div>
@@ -132,9 +88,9 @@ export default function Home() {
transition={{ duration: 0.6 }}
className="mb-16"
>
<h2 className="text-4xl font-bold mb-4">What I Build</h2>
<h2 className="text-4xl font-bold mb-4">{t.projects.title}</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Production systems spanning infrastructure, distributed backends, and LLM optimization. Click any domain to explore.
{t.projects.subtitle}
</p>
</motion.div>
@@ -147,14 +103,20 @@ export default function Home() {
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
staggerChildren: 0.15,
},
},
}}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
className="grid grid-cols-1 gap-8 max-w-3xl mx-auto"
>
{features.map((feature) => (
<FeatureCard key={feature.title} {...feature} />
{projects.map((project) => (
<ProjectShowcase
key={project.title}
{...project}
watchVideoText={t.projects.watchVideo}
readArticleText={t.projects.readArticle}
askPoimenText={t.projects.askPoimen}
/>
))}
</motion.div>
</section>
@@ -162,14 +124,14 @@ export default function Home() {
{/* Footer */}
<footer className="border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 py-12 px-6">
<div className="max-w-6xl mx-auto text-center text-sm text-gray-600 dark:text-gray-400">
<p>© 2025 Rock Liang. Deployed on homelab Kubernetes cluster (3-node Talos).</p>
<p>{t.footer.copyright}</p>
<p className="mt-2">
<a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400">
GitHub
<a href="https://github.com/rockliang" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400">
{t.footer.github}
</a>
{' • '}
<a href="mailto:[email protected]" className="hover:text-blue-600 dark:hover:text-blue-400">
Email
{t.footer.email}
</a>
</p>
</div>
@@ -177,6 +139,70 @@ export default function Home() {
{/* Interactive Terminal */}
<InteractiveTerminal />
{/* Bio Modal */}
{bioModalOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
onClick={() => setBioModalOpen(false)}
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-2xl w-full max-h-96 overflow-y-auto"
>
{/* Header */}
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-purple-600 px-6 py-6 flex justify-between items-center">
<h2 className="text-2xl font-bold text-white">{t.bio.title}</h2>
<button
onClick={() => setBioModalOpen(false)}
className="text-white hover:bg-white/20 rounded-lg p-2 transition"
>
<X size={24} />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-4 text-gray-900 dark:text-white">
<p className="text-lg">
{t.bio.intro}
</p>
<div>
<h3 className="font-semibold text-blue-600 dark:text-blue-400 mb-2">{t.bio.expertise.title}</h3>
<p className="text-sm text-gray-700 dark:text-gray-300">
{t.bio.expertise.content}
</p>
</div>
<div>
<h3 className="font-semibold text-blue-600 dark:text-blue-400 mb-2">{t.bio.highlights.title}</h3>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-1">
{t.bio.highlights.items.map((item, i) => (
<li key={i}> {item}</li>
))}
</ul>
</div>
<div>
<h3 className="font-semibold text-blue-600 dark:text-blue-400 mb-2">{t.bio.educationTitle}</h3>
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line">
{t.bio.educationContent}
</p>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 pt-4 border-t border-gray-200 dark:border-gray-700">
{t.bio.currentFocus}
</p>
</div>
</motion.div>
</motion.div>
)}
</main>
)
}
+51
View File
@@ -0,0 +1,51 @@
'use client'
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
export default function TerraformDriftPage() {
return (
<main className="min-h-screen bg-white dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-6 py-20">
<Link
href="/#project-rbc"
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-8"
>
<ArrowLeft size={16} />
Back to Projects
</Link>
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
Terraform State Drift Solution
</h1>
<p className="text-gray-600 dark:text-gray-400 mb-12">
Coming soon...
</p>
{/* Placeholder sections */}
<div className="space-y-8 text-gray-500 dark:text-gray-500">
<div className="border border-dashed border-gray-300 dark:border-gray-700 rounded-lg p-6">
<h2 className="font-semibold mb-2">The Problem</h2>
<p className="text-sm">Content coming soon...</p>
</div>
<div className="border border-dashed border-gray-300 dark:border-gray-700 rounded-lg p-6">
<h2 className="font-semibold mb-2">Immediate Fix</h2>
<p className="text-sm">Content coming soon...</p>
</div>
<div className="border border-dashed border-gray-300 dark:border-gray-700 rounded-lg p-6">
<h2 className="font-semibold mb-2">Short Term Fix</h2>
<p className="text-sm">Content coming soon...</p>
</div>
<div className="border border-dashed border-gray-300 dark:border-gray-700 rounded-lg p-6">
<h2 className="font-semibold mb-2">Long Term Fix</h2>
<p className="text-sm">Content coming soon...</p>
</div>
</div>
</div>
</main>
)
}
Executable
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
REGISTRY="${REGISTRY:-forgejo.riotpiao.com}"
IMAGE="${REGISTRY}/rock/portfolio:latest"
echo "Building image: ${IMAGE}"
docker buildx build --platform linux/amd64 -t "${IMAGE}" .
echo "Pushing image..."
docker push "${IMAGE}"
echo "✓ Successfully pushed ${IMAGE}"
+49 -55
View File
@@ -1,54 +1,38 @@
'use client'
import { motion } from 'framer-motion'
import { useLanguage } from '@/lib/LanguageContext'
interface TimelineItem {
company: string
role: string
period: string
description: string
impact: string
skills: string[]
color: string
}
const colors = [
'from-green-500 to-green-600',
'from-red-500 to-red-600',
'from-orange-500 to-orange-600',
'from-cyan-500 to-cyan-600',
'from-blue-500 to-blue-600',
]
const timeline: TimelineItem[] = [
{
company: 'RBC',
role: 'Infrastructure Platform Architect',
period: '2020 — Present',
description: 'Leading multi-region infrastructure platform on Kubernetes. Building GitOps workflows with Terraform and Argo CD across 4 global regions.',
impact: '40% CPU reduction, 99.2% uptime',
skills: ['Kubernetes', 'Terraform', 'Argo CD', 'Go', 'AWS', 'Cilium', 'Prometheus', 'Grafana'],
color: 'from-blue-500 to-blue-600',
},
{
company: 'AWS',
role: 'Senior Software Engineer (Step Functions)',
period: '2019 — 2020',
description: 'Designed and optimized distributed orchestration platform. Built fault-tolerant task scheduling across 57+ regions serving mission-critical workloads.',
impact: '8 core components, 57+ regions',
skills: ['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'],
color: 'from-orange-500 to-orange-600',
},
{
company: 'Homelab',
role: 'DevOps / SRE / SDE',
period: '2021 — Present',
description: 'Production-grade bare-metal K8s cluster (Talos + Cilium). Built Temporal-based LLM inference pipeline with CPU-bound optimization and quantization.',
impact: '60% latency cut, 75% memory saved',
skills: ['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability'],
color: 'from-green-500 to-green-600',
},
{
company: 'Open Source',
role: 'Maintainer — go-flink',
period: '2022 — Present',
description: 'Building distributed DataLakeHouse framework in Go. Fault-tolerant task scheduling, streaming semantics, and efficient data processing.',
impact: 'Public repository, active development',
skills: ['Go', 'Distributed Systems', 'Data Pipeline', 'Testing', 'Architecture'],
color: 'from-purple-500 to-purple-600',
},
const skills = [
['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability', 'Go-Flink'],
['Golang', 'Terraform', 'IaC', 'OpenShift', 'Docker', 'Distributed Systems', 'Grafana'],
['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'],
['Machine Learning', 'Golang', 'Anomaly Detection', 'C++', 'CMake', 'Data Security'],
['Django', 'React', 'Agile', 'Web Development', 'Sonar', 'ATC Systems'],
]
const links = [
null,
'#project-rbc', // RBC - link to RBC project showcase
null,
null,
'https://plan.navcanada.ca/account/login/?next=/',
]
const linkTexts = [
'',
'Learn more →',
'',
'',
'View Product →',
]
const containerVariants = {
@@ -71,6 +55,9 @@ const itemVariants = {
}
export function ExperienceTimeline() {
const { t } = useLanguage()
const timeline = t.experience.items
return (
<section className="max-w-4xl mx-auto px-6 py-20">
<motion.div
@@ -80,9 +67,9 @@ export function ExperienceTimeline() {
transition={{ duration: 0.6 }}
className="mb-16"
>
<h2 className="text-4xl font-bold mb-4">Explore Experience</h2>
<h2 className="text-4xl font-bold mb-4">{t.experience.title}</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
From AWS to RBC. Building systems at scale. Skills acquired along the journey.
{t.experience.subtitle}
</p>
</motion.div>
@@ -105,7 +92,7 @@ export function ExperienceTimeline() {
className="relative pl-8 md:pl-0"
>
{/* Timeline dot */}
<div className={`absolute left-0 md:left-1/2 top-2 w-4 h-4 rounded-full bg-gradient-to-br ${item.color} transform md:-translate-x-1/2 -translate-x-1.5 border-4 border-white dark:border-gray-950 shadow-lg`} />
<div className={`absolute left-0 md:left-1/2 top-2 w-4 h-4 rounded-full bg-gradient-to-br ${colors[index]} transform md:-translate-x-1/2 -translate-x-1.5 border-4 border-white dark:border-gray-950 shadow-lg`} />
{/* Content card */}
<div className={index % 2 === 0 ? 'md:w-1/2 md:pr-8' : 'md:w-1/2 md:ml-auto md:pl-8'}>
@@ -128,17 +115,24 @@ export function ExperienceTimeline() {
{item.description}
</p>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 rounded px-3 py-2 mb-4 text-sm font-semibold text-blue-600 dark:text-blue-400">
📊 {item.impact}
</div>
<div className="flex flex-wrap gap-2">
{item.skills.map((skill) => (
<div className="flex flex-wrap gap-2 mb-4">
{skills[index]?.map((skill) => (
<span key={skill} className="text-xs px-3 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
{skill}
</span>
))}
</div>
{links[index] && (
<a
href={links[index]!}
target={links[index]!.startsWith('#') ? '_self' : '_blank'}
rel="noopener noreferrer"
className="text-xs font-semibold text-blue-600 dark:text-blue-400 hover:underline"
>
{linkTexts[index] || 'View Product →'}
</a>
)}
</div>
</div>
</motion.div>
+310 -18
View File
@@ -1,12 +1,31 @@
'use client'
import Link from 'next/link'
import Image from 'next/image'
import { Menu, X, Moon, Sun } from 'lucide-react'
import { useState, useEffect } from 'react'
import { useTerminal } from '@/lib/TerminalContext'
import { useLanguage } from '@/lib/LanguageContext'
export default function Header() {
const [open, setOpen] = useState(false)
const [dark, setDark] = useState(false)
const [skillsOpen, setSkillsOpen] = useState(false)
const [contactOpen, setContactOpen] = useState(false)
const [educationOpen, setEducationOpen] = useState(false)
const { isOpen: terminalOpen, setIsOpen: setTerminalOpen } = useTerminal()
const { lang, setLang, t } = useLanguage()
const [copied, setCopied] = useState<string | null>(null)
const handleCopy = (text: string, label: string) => {
navigator.clipboard.writeText(text)
setCopied(label)
setTimeout(() => setCopied(null), 2000)
}
const toggleLang = () => {
setLang(lang === 'en' ? 'zh' : 'en')
}
useEffect(() => {
const isDark = document.documentElement.classList.contains('dark')
@@ -22,42 +41,315 @@ export default function Header() {
<header className="sticky top-0 z-50 border-b border-gray-200 dark:border-gray-800 bg-white/95 dark:bg-gray-950/95 backdrop-blur">
<div className="max-w-6xl mx-auto px-6 py-4 flex justify-between items-center">
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center text-white font-bold text-sm">
RL
</div>
<Image
src="/avatar.jpeg"
alt="Profile"
width={32}
height={32}
className="rounded-full object-cover"
/>
<div>
<div className="text-sm font-bold text-gray-900 dark:text-white">Rock Liang</div>
<div className="text-xs text-gray-600 dark:text-gray-400">Software Engineer Lead</div>
<div className="text-sm font-bold text-gray-900 dark:text-white">{t.header.title}</div>
<div className="text-xs text-gray-600 dark:text-gray-400">{t.header.subtitle}</div>
</div>
</Link>
<nav className="hidden md:flex gap-8 items-center">
<Link href="/" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Home</Link>
<Link href="/cluster" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Cluster</Link>
<Link href="/infrastructure" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Infrastructure</Link>
<Link href="/kafka" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Kafka</Link>
<Link href="/cicd" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">CI/CD</Link>
<Link href="/agent" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Agent</Link>
<Link href="/" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">{t.header.nav.home}</Link>
<button
onClick={() => setTerminalOpen(!terminalOpen)}
className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
>
{t.header.nav.askPoimen}
</button>
<div className="relative">
<button
onClick={() => setSkillsOpen(!skillsOpen)}
className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
>
{t.header.nav.skills}
</button>
{skillsOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg p-4 space-y-2">
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400">{t.header.skillsPlaceholder}</div>
</div>
)}
</div>
<div className="relative">
<button
onClick={() => setContactOpen(!contactOpen)}
className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
>
{t.header.nav.contact}
</button>
{contactOpen && (
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg p-4 space-y-3">
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 pb-3">
<span className="text-sm text-gray-700 dark:text-gray-300 flex-1">
{t.header.contact.portfolio}
</span>
<button
onClick={() => handleCopy('https://portfolio.riotpiao.com', 'Portfolio')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'Portfolio' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="https://github.com/rockliang"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1"
>
{t.header.contact.github}
</a>
<button
onClick={() => handleCopy('https://github.com/rockliang', 'GitHub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'GitHub' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="https://github.com/rockliang"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1"
>
{t.header.contact.privateGithub}
</a>
<button
onClick={() => handleCopy('https://github.com/rockliang', 'PrivateGithub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'PrivateGithub' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="https://www.linkedin.com/in/rockliang"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1"
>
{t.header.contact.linkedin}
</a>
<button
onClick={() => handleCopy('https://www.linkedin.com/in/rockliang', 'LinkedIn')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'LinkedIn' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="mailto:[email protected]"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1"
>
{t.header.contact.email}
</a>
<button
onClick={() => handleCopy('[email protected]', 'Email')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'Email' ? '✓' : t.header.contact.copy}
</button>
</div>
</div>
)}
</div>
<div className="border-l border-gray-300 dark:border-gray-700 pl-8 flex gap-6 items-center">
<div className="text-xs">
<div className="text-gray-600 dark:text-gray-400">{t.header.experience}</div>
<div className="font-semibold text-gray-900 dark:text-white">{t.header.experienceValue}</div>
</div>
<div className="relative">
<button
onClick={() => setEducationOpen(!educationOpen)}
className="text-xs cursor-pointer hover:text-blue-600 dark:hover:text-blue-400"
>
<div className="text-gray-600 dark:text-gray-400">{t.header.education}</div>
<div className="font-semibold text-gray-900 dark:text-white">{t.header.educationValue}</div>
</button>
{educationOpen && (
<div className="absolute right-0 mt-2 w-72 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg p-4 space-y-4">
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
<h3 className="font-semibold text-gray-900 dark:text-white">{t.header.educationDetails.bachelor.school}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{t.header.educationDetails.bachelor.degree}</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">{t.header.educationDetails.bachelor.period}</p>
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">{t.header.educationDetails.master.school}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{t.header.educationDetails.master.degree}</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">{t.header.educationDetails.master.period}</p>
</div>
</div>
)}
</div>
<button onClick={toggleDark} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded">
{dark ? <Sun size={20} /> : <Moon size={20} />}
</button>
{/* Language Toggle */}
<button
onClick={toggleLang}
className="px-3 py-1 text-sm font-semibold rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800"
>
{lang === 'en' ? '中文' : 'EN'}
</button>
</div>
</nav>
<div className="flex items-center gap-2 md:hidden">
{/* Mobile Language Toggle */}
<button
onClick={toggleLang}
className="px-2 py-1 text-xs font-semibold rounded border border-gray-300 dark:border-gray-600"
>
{lang === 'en' ? '中文' : 'EN'}
</button>
<button
onClick={() => setOpen(!open)}
className="md:hidden p-2"
className="p-2"
>
{open ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
{open && (
<div className="md:hidden border-t border-gray-200 dark:border-gray-800 p-4 space-y-2">
<Link href="/" className="block py-2 text-sm hover:text-blue-600">Home</Link>
<Link href="/infrastructure" className="block py-2 text-sm hover:text-blue-600">Infrastructure</Link>
<Link href="/kafka" className="block py-2 text-sm hover:text-blue-600">Kafka</Link>
<Link href="/cicd" className="block py-2 text-sm hover:text-blue-600">CI/CD</Link>
<Link href="/agent" className="block py-2 text-sm hover:text-blue-600">Agent</Link>
<div className="md:hidden border-t border-gray-200 dark:border-gray-800 p-4 space-y-4">
<Link href="/" className="block py-2 text-sm hover:text-blue-600">{t.header.nav.home}</Link>
<button
onClick={() => setTerminalOpen(!terminalOpen)}
className="block py-2 text-sm hover:text-blue-600"
>
{t.header.nav.askPoimen}
</button>
<button
onClick={() => setSkillsOpen(!skillsOpen)}
className="block py-2 text-sm hover:text-blue-600"
>
{t.header.nav.skills}
</button>
{skillsOpen && (
<div className="bg-gray-50 dark:bg-gray-800 rounded p-3 mt-2 space-y-2">
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400">{t.header.skillsPlaceholder}</div>
</div>
)}
<button
onClick={() => setContactOpen(!contactOpen)}
className="block py-2 text-sm hover:text-blue-600"
>
{t.header.nav.contact}
</button>
{contactOpen && (
<div className="bg-gray-50 dark:bg-gray-800 rounded p-3 mt-2 space-y-3">
<div className="flex items-center justify-between border-b border-gray-300 dark:border-gray-700 pb-3">
<span className="text-sm text-gray-700 dark:text-gray-300 flex-1">
{t.header.contact.portfolio}
</span>
<button
onClick={() => handleCopy('https://portfolio.riotpiao.com', 'Portfolio')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'Portfolio' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="https://github.com/rockliang"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1"
>
{t.header.contact.github}
</a>
<button
onClick={() => handleCopy('https://github.com/rockliang', 'GitHub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'GitHub' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="https://github.com/rockliang"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1"
>
{t.header.contact.privateGithub}
</a>
<button
onClick={() => handleCopy('https://github.com/rockliang', 'PrivateGithub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'PrivateGithub' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="https://www.linkedin.com/in/rockliang"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1"
>
{t.header.contact.linkedin}
</a>
<button
onClick={() => handleCopy('https://www.linkedin.com/in/rockliang', 'LinkedIn')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'LinkedIn' ? '✓' : t.header.contact.copy}
</button>
</div>
<div className="flex items-center justify-between">
<a
href="mailto:[email protected]"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1"
>
{t.header.contact.email}
</a>
<button
onClick={() => handleCopy('[email protected]', 'Email')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
>
{copied === 'Email' ? '✓' : t.header.contact.copy}
</button>
</div>
</div>
)}
<div className="border-t border-gray-200 dark:border-gray-800 pt-4 space-y-4">
<div className="text-xs">
<div className="text-gray-600 dark:text-gray-400">{t.header.experience}</div>
<div className="font-semibold text-gray-900 dark:text-white">{t.header.experienceValue}</div>
</div>
<div>
<button
onClick={() => setEducationOpen(!educationOpen)}
className="text-xs cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 w-full text-left"
>
<div className="text-gray-600 dark:text-gray-400">{t.header.education}</div>
<div className="font-semibold text-gray-900 dark:text-white">{t.header.educationValue}</div>
</button>
{educationOpen && (
<div className="bg-gray-50 dark:bg-gray-800 rounded p-3 mt-2 space-y-4">
<div className="border-b border-gray-300 dark:border-gray-700 pb-4">
<h3 className="font-semibold text-gray-900 dark:text-white text-sm">{t.header.educationDetails.bachelor.school}</h3>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">{t.header.educationDetails.bachelor.degree}</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">{t.header.educationDetails.bachelor.period}</p>
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white text-sm">{t.header.educationDetails.master.school}</h3>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">{t.header.educationDetails.master.degree}</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">{t.header.educationDetails.master.period}</p>
</div>
</div>
)}
</div>
</div>
</div>
)}
</header>
+52 -38
View File
@@ -2,62 +2,74 @@
import { motion, AnimatePresence } from 'framer-motion'
import { useState, useEffect } from 'react'
import { Code, Network, Zap, Sparkles, Github } from 'lucide-react'
import { TypewriterText } from './TypewriterText'
import Image from 'next/image'
import { Code, Network, Zap, Sparkles, Shield, Cpu } from 'lucide-react'
import { useLanguage } from '@/lib/LanguageContext'
const roles = [
{ text: 'Software Engineer Lead', icon: Code },
{ text: 'Infrastructure Architect', icon: Network },
{ text: 'Distributed Systems Builder', icon: Zap },
{ text: 'LLM Systems Optimizer', icon: Sparkles },
{ text: 'Open Source Developer', icon: Github },
]
const roleIcons = [Network, Shield, Code, Sparkles, Zap, Cpu]
export function HeroBlobFlow() {
const [index, setIndex] = useState(0)
const [isDark, setIsDark] = useState(false)
const { t } = useLanguage()
const roles = t.hero.roles
useEffect(() => {
const isDarkMode = document.documentElement.classList.contains('dark')
setIsDark(isDarkMode)
const observer = new MutationObserver(() => {
const darkMode = document.documentElement.classList.contains('dark')
setIsDark(darkMode)
})
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
})
return () => observer.disconnect()
}, [])
useEffect(() => {
const interval = setInterval(() => {
setIndex((prev) => (prev + 1) % roles.length)
}, 2000)
return () => clearInterval(interval)
}, [])
}, [roles.length])
return (
<div className="relative w-full max-w-md mx-auto aspect-square flex flex-col items-center justify-center">
{/* Background glow */}
<div className="absolute inset-0">
<motion.div
animate={{
scale: [1, 1.15, 1],
opacity: [0.25, 0.4, 0.25],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute inset-0 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-full blur-3xl"
<div className="relative w-full max-w-5xl mx-auto">
{/* Full hire-me image */}
<div className="relative w-full">
<Image
src={isDark ? '/hire-me.png' : '/hire-me-light.png'}
alt="You hire me, you get a bundle"
width={1200}
height={800}
className="w-full h-auto"
priority
/>
</div>
{/* Avatar center */}
{/* Avatar + Rotating text - centered on image */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20 flex flex-col items-center">
{/* Avatar */}
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="relative z-10 w-32 h-32"
>
<div className="w-full h-full rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center text-white font-bold text-4xl border-4 border-white dark:border-gray-950 shadow-xl">
RL
</div>
<Image
src="/avatar.jpeg"
alt="Profile"
width={200}
height={200}
className="w-48 h-48 rounded-full object-cover border-4 border-white dark:border-gray-950 shadow-xl"
/>
</motion.div>
{/* Typewriter text below avatar */}
<TypewriterText />
{/* Rotating role text */}
<div className="relative z-10 mt-12 px-6 w-full flex items-center justify-center">
<div className="mt-6 h-16 flex items-center justify-center">
<AnimatePresence mode="wait">
<motion.div
key={index}
@@ -65,16 +77,18 @@ export function HeroBlobFlow() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
transition={{ duration: 0.6, ease: 'easeInOut' }}
className="absolute text-2xl font-semibold text-gray-900 dark:text-white text-center px-6 flex items-center gap-3 justify-center"
className="text-3xl font-bold text-gray-900 dark:text-white text-center flex items-center gap-3 justify-center bg-white/80 dark:bg-gray-900/80 rounded-xl py-3 px-6 backdrop-blur-sm"
>
{(() => {
const Icon = roles[index].icon
return <Icon size={28} className="text-blue-600 dark:text-blue-400 flex-shrink-0" />
const Icon = roleIcons[index]
return <Icon size={32} className="text-blue-600 dark:text-blue-400 flex-shrink-0" />
})()}
{roles[index].text}
{roles[index]}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
)
}
+10 -4
View File
@@ -2,6 +2,7 @@
import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import { useTerminal } from '@/lib/TerminalContext'
const commands: Record<string, string> = {
help: `Available commands:
@@ -46,10 +47,15 @@ Open Source: go-flink (distributed DataLakeHouse)`,
export function InteractiveTerminal() {
const [input, setInput] = useState('')
const [history, setHistory] = useState<{ cmd: string; output: string }[]>([])
const [isOpen, setIsOpen] = useState(false)
const { isOpen, setIsOpen } = useTerminal()
const [isMac, setIsMac] = useState(true)
const terminalRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// Detect if user is on Mac
const isMacOS = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
setIsMac(isMacOS)
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
@@ -59,7 +65,7 @@ export function InteractiveTerminal() {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen])
}, [isOpen, setIsOpen])
const handleCommand = (cmd: string) => {
const trimmed = cmd.trim().toLowerCase()
@@ -89,7 +95,7 @@ export function InteractiveTerminal() {
onClick={() => setIsOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-mono text-sm shadow-lg"
>
Terminal (Cmd+K)
Ask Poimen ({isMac ? 'Cmd' : 'Ctrl'}+K)
</button>
) : (
<motion.div
@@ -98,7 +104,7 @@ export function InteractiveTerminal() {
className="bg-gray-950 border border-gray-700 rounded-lg shadow-2xl w-96 h-96 flex flex-col"
>
<div className="bg-gray-900 border-b border-gray-700 px-4 py-3 flex justify-between items-center">
<span className="text-gray-300 font-mono text-xs">ZSH_THEME="robbyrussell"</span>
<span className="text-gray-300 font-mono text-xs">Poimen (Agent Terminal)</span>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-white"
+200
View File
@@ -0,0 +1,200 @@
'use client'
import { motion } from 'framer-motion'
import { ExternalLink, MessageSquare } from 'lucide-react'
import { useTerminal } from '@/lib/TerminalContext'
interface ProjectShowcaseProps {
id?: string
title: string
description: string
longDescription?: string
stat: string
highlight?: string
status: 'live' | 'building' | 'planning' | 'completed'
media?: {
type: 'image' | 'video'
url: string
}
videoUrl?: string
articleUrl?: string
watchVideoText?: string
readArticleText?: string
askPoimenText?: string
bullets?: string[]
deepDive?: {
label: string
url: string
}
}
export function ProjectShowcase({
id,
title,
description,
longDescription,
stat,
highlight,
status,
media,
videoUrl,
articleUrl,
watchVideoText = 'Watch Video',
readArticleText = 'Read Article',
askPoimenText = 'Ask Poimen for technical details',
bullets,
deepDive,
}: ProjectShowcaseProps) {
const { setIsOpen } = useTerminal()
const statusColors = {
live: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
completed: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
building: 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
planning: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
}
return (
<motion.div
id={id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow"
>
{/* Optional Media (Image or Video) */}
{media && (
<div className="aspect-video bg-gradient-to-br from-blue-500 to-purple-500 dark:from-blue-900 dark:to-purple-900 flex items-center justify-center">
{media.type === 'video' ? (
(() => {
const youtubeMatch = media.url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)
if (youtubeMatch) {
return (
<iframe
src={`https://www.youtube.com/embed/${youtubeMatch[1]}`}
title={title}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
)
}
return null
})()
) : (
<img src={media.url} alt={title} className="w-full h-full object-cover" />
)}
</div>
)}
{/* Content */}
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-2">
{description}
</p>
{longDescription && (
<p className="text-sm text-gray-600 dark:text-gray-400">
{longDescription}
</p>
)}
{bullets && bullets.length > 0 && (
<ul className="mt-3 space-y-1">
{bullets.map((bullet, i) => {
// Highlight skills: parentheses + keywords
const skillKeywords = ['Terraform', 'Golang', 'K8s', 'Slack', 'Grafana', 'IaC', 'Docker', 'OpenShift', 'Temporal', 'Kafka', 'gRPC', 'AWS', 'Java', 'Python', 'Go', 'C++']
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const pattern = new RegExp(`(\\([^)]+\\)|${skillKeywords.map(escapeRegex).join('|')})`, 'g')
const parts = bullet.split(pattern).filter(Boolean)
return (
<li key={i} className="text-sm text-gray-600 dark:text-gray-400 flex items-start gap-2">
<span className="text-blue-500"></span>
<span>
{parts.map((part, j) =>
(part.startsWith('(') && part.endsWith(')')) || skillKeywords.includes(part) ? (
<span key={j} className="text-blue-600 dark:text-blue-400 font-medium">
{part}
</span>
) : (
<span key={j}>{part}</span>
)
)}
</span>
</li>
)
})}
</ul>
)}
{deepDive && (
<a
href={deepDive.url}
className="mt-3 inline-block text-sm font-semibold text-blue-600 dark:text-blue-400 hover:underline"
>
{deepDive.label}
</a>
)}
</div>
<span
className={`ml-4 px-3 py-1 rounded-full text-xs font-semibold whitespace-nowrap ${
statusColors[status]
}`}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
</div>
{/* Stat */}
<div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 rounded px-4 py-3 mb-4 space-y-1">
<p className="text-sm font-semibold text-blue-600 dark:text-blue-400">
📊 {stat}
</p>
{highlight && (
<p className="text-sm font-semibold text-blue-600 dark:text-blue-400">
📊 {highlight}
</p>
)}
</div>
{/* CTA Buttons */}
<div className="flex gap-3">
{videoUrl && videoUrl !== '#' && !videoUrl.includes('youtube.com') && !videoUrl.includes('youtu.be') && (
<a
href={videoUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold transition flex items-center justify-center gap-2"
>
<span>{watchVideoText}</span>
<ExternalLink size={16} />
</a>
)}
{articleUrl && articleUrl !== '#' && (
<a
href={articleUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 border-2 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 px-4 py-2 rounded-lg font-semibold transition flex items-center justify-center gap-2"
>
<span>{readArticleText}</span>
<ExternalLink size={16} />
</a>
)}
</div>
{/* Ask Poimen */}
<button
onClick={() => setIsOpen(true)}
className="mt-4 w-full text-sm text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 flex items-center justify-center gap-2 transition"
>
<MessageSquare size={14} />
{askPoimenText}
</button>
</div>
</motion.div>
)
}
+3 -3
View File
@@ -6,7 +6,7 @@ metadata:
spec:
project: default
source:
repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
targetRevision: main
path: infra/portfolio/base
destination:
@@ -27,7 +27,7 @@ metadata:
spec:
project: default
source:
repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
targetRevision: main
path: infra/homarr/base
destination:
@@ -48,7 +48,7 @@ metadata:
spec:
project: default
source:
repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
targetRevision: main
path: infra/auth-infra/base
destination:
+9 -9
View File
@@ -5,17 +5,17 @@ metadata:
namespace: longhorn-system
annotations:
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- longhorn.riotpiao.homelab.com
- longhorn.riotpiao.com
secretName: longhorn-tls
rules:
- host: longhorn.riotpiao.homelab.com
- host: longhorn.riotpiao.com
http:
paths:
- path: /
@@ -33,17 +33,17 @@ metadata:
namespace: dashboard
annotations:
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- portainer.riotpiao.homelab.com
- portainer.riotpiao.com
secretName: portainer-tls
rules:
- host: portainer.riotpiao.homelab.com
- host: portainer.riotpiao.com
http:
paths:
- path: /
@@ -61,17 +61,17 @@ metadata:
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- prometheus.riotpiao.homelab.com
- prometheus.riotpiao.com
secretName: prometheus-tls
rules:
- host: prometheus.riotpiao.homelab.com
- host: prometheus.riotpiao.com
http:
paths:
- path: /
+2 -2
View File
@@ -12,8 +12,8 @@ spec:
env:
AUTH_PROVIDERS: "oidc"
AUTH_OIDC_CLIENT_ID: "homarr"
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.homelab.com/application/o/homarr/"
AUTH_OIDC_URI: "https://authentik.riotpiao.homelab.com/application/o/homarr/.well-known/openid-configuration"
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.com/application/o/homarr/"
AUTH_OIDC_URI: "https://authentik.riotpiao.com/application/o/homarr/.well-known/openid-configuration"
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
envFrom:
- secretRef:
+2 -2
View File
@@ -9,10 +9,10 @@ spec:
ingressClassName: nginx
tls:
- hosts:
- homarr.riotpiao.homelab.com
- homarr.riotpiao.com
secretName: homarr-tls
rules:
- host: homarr.riotpiao.homelab.com
- host: homarr.riotpiao.com
http:
paths:
- path: /
+1 -1
View File
@@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: portfolio
image: forgejo.riotpiao.homelab.com/rock/portfolio:latest
image: forgejo.riotpiao.com/rock/portfolio:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
+2 -5
View File
@@ -3,16 +3,13 @@ kind: Ingress
metadata:
name: portfolio
namespace: portfolio
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- portfolio.riotpiao.homelab.com
- riotpiao.com
secretName: portfolio-tls
rules:
- host: portfolio.riotpiao.homelab.com
- host: riotpiao.com
http:
paths:
- path: /
+34
View File
@@ -0,0 +1,34 @@
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
import translations from './translations.json'
type Language = 'en' | 'zh'
interface LanguageContextType {
lang: Language
setLang: (lang: Language) => void
t: typeof translations.en
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined)
export function LanguageProvider({ children }: { children: ReactNode }) {
const [lang, setLang] = useState<Language>('en')
const t = lang === 'zh' ? translations.zh : translations.en
return (
<LanguageContext.Provider value={{ lang, setLang, t }}>
{children}
</LanguageContext.Provider>
)
}
export function useLanguage() {
const context = useContext(LanguageContext)
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider')
}
return context
}
+28
View File
@@ -0,0 +1,28 @@
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
interface TerminalContextType {
isOpen: boolean
setIsOpen: (open: boolean) => void
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined)
export function TerminalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
return (
<TerminalContext.Provider value={{ isOpen, setIsOpen }}>
{children}
</TerminalContext.Provider>
)
}
export function useTerminal() {
const context = useContext(TerminalContext)
if (!context) {
throw new Error('useTerminal must be used within TerminalProvider')
}
return context
}
+354
View File
@@ -0,0 +1,354 @@
{
"en": {
"header": {
"title": "Rock Liang",
"subtitle": "Senior Software Engineer",
"nav": {
"home": "Home",
"askPoimen": "AskPoimen",
"skills": "Skills",
"contact": "Contact"
},
"experience": "Experience",
"experienceValue": "6+ YoE",
"education": "Education",
"educationValue": "M.Sc. UOttawa",
"educationDetails": {
"bachelor": {
"school": "University of Ottawa",
"degree": "Bachelor's Degree, Computer Science",
"period": "2016 2019"
},
"master": {
"school": "University of Ottawa",
"degree": "Master's degree, Computer Science",
"period": "Sep 2019 Nov 2021"
}
},
"contact": {
"portfolio": "portfolio.riotpiao.com",
"github": "GitHub",
"privateGithub": "Private Github",
"linkedin": "LinkedIn",
"email": "Email",
"copy": "Copy"
},
"skillsPlaceholder": "Skills will appear here"
},
"hero": {
"title": "Senior Software Engineer",
"subtitle": "Infrastructure × Backend × LLM Systems",
"description": "Building observable, scalable, fault-tolerant systems for mass audience.",
"exploreMore": "Explore more",
"terminalHint": "Press",
"terminalHintSuffix": "to explore via terminal",
"roles": [
"Infrastructure",
"SRE",
"SDE",
"Agentic Engineer",
"Distributed Systems",
"System/Platform Engineer"
]
},
"bio": {
"title": "About Me",
"intro": "Senior Software Engineer with 6+ years of experience building observable, scalable, and fault-tolerant systems for mass audiences.",
"expertise": {
"title": "Technical Expertise",
"content": "Infrastructure automation (Terraform, Kubernetes, ArgoCD) • Distributed systems (gRPC, Kafka, AWS Step Functions) • LLM inference optimization (INT4/INT8 quantization) • Backend systems (Go, Java, Python, C++)"
},
"highlights": {
"title": "Career Highlights",
"items": [
"Launched AWS Distributed-Map service across 57+ regions",
"Built multi-region infrastructure at RBC (40% cost reduction)",
"Architected production homelab: Kubernetes, Kafka, LLM inference",
"Open source: go-flink (distributed DataLakeHouse)"
]
},
"educationTitle": "Education",
"educationContent": "M.Sc. Computer Science, University of Ottawa (20192021)\nB.Sc. Computer Science, University of Ottawa (20162019)",
"currentFocus": "Currently focused on infrastructure automation, LLM systems optimization, and building the next generation of distributed systems."
},
"projects": {
"title": "What have i Built and Building",
"subtitle": "Demonstrate my work",
"watchVideo": "Watch Video",
"readArticle": "Read Article",
"askPoimen": "Ask Poimen for technical details",
"items": [
{
"title": "AWS Distributed-Map",
"description": "Launched distributed task orchestration service for mission-critical workloads.",
"longDescription": "Optimized execution across 57+ regions with fault-tolerant scheduling and auto-scaling.",
"stat": "Production launch, 57+ regions"
},
{
"title": "RBC: Multi-Cloud Platform",
"description": "Unified infrastructure platform consolidating public cloud and on-prem.",
"longDescription": "Terraform automation with Temporal orchestration. 99.2% automated provisioning, notification-driven operator fallback.",
"stat": "200+ microservices, 4 regions, 2hr→20min deploy",
"highlight": "Standardized IaC patterns across 12 teams—cut provisioning toil, 3x integration velocity.",
"bullets": [
"Integrated Terraform Cloud to centralize IaC workflows—reduced onboarding time for 12 teams",
"Built K8s CronJob to detect and reconcile Terraform state drift automatically (Golang)",
"Designed Slack notification service with Golang workers—operators resolve apply failures in <5min",
"Led requirement gathering across 4 platform teams—disambiguated specs, unblocked 3 stalled projects",
"Translated technical decisions for non-technical stakeholders—secured buy-in for platform migration"
],
"deepDive": {
"label": "Deep Dive: Terraform State Drift Solution →",
"url": "/terraform-drift"
}
},
{
"title": "Homelab: Kubernetes Cluster",
"description": "Bare-metal Kubernetes cluster running production workloads.",
"longDescription": "Talos OS with 3-node control plane. Cilium eBPF CNI, Longhorn storage, ArgoCD GitOps.",
"stat": "3-node cluster, 99.2% uptime"
},
{
"title": "Homelab: LLM Inference",
"description": "Production LLM inference pipeline with CPU optimization.",
"longDescription": "Temporal-based orchestration with INT4/INT8 quantization. Achieved 60% latency reduction.",
"stat": "60% latency cut, real-time inference"
},
{
"title": "Homelab: Kafka Cluster",
"description": "High-throughput event streaming with Strimzi KRaft.",
"longDescription": "3-broker KRaft cluster with auto-scaling, persistent storage, and monitoring.",
"stat": "3 brokers, 1K+ msgs/sec"
},
{
"title": "Open Source: go-flink",
"description": "Distributed DataLakeHouse framework written in Go.",
"longDescription": "Fault-tolerant data pipelines with streaming semantics and efficient processing.",
"stat": "Public repository, active"
}
]
},
"experience": {
"title": "Explore Experience",
"subtitle": "Feel free to Ask Poimen more abt it",
"items": [
{
"company": "riotpiao.com",
"role": "DevOps / SRE / SDE",
"period": "May 2026 — Present",
"description": "Built and maintain production-grade homelab on bare-metal Kubernetes (3-node Talos cluster). Architected Temporal-based LLM inference pipelines with 60% latency reduction. Full GitOps automation with ArgoCD, Terraform, and custom operators."
},
{
"company": "RBC",
"role": "Lead Software Engineer",
"period": "Nov 2024 — May 2026",
"description": "Deployments were manual and slow—teams blocked 2+ hours waiting. Chose Terraform for idempotent drift handling, Temporal for multi-cloud orchestration with built-in retry. Cut deploy time to 20min, consolidated public cloud and on-prem into single platform with 99.2% automation and notification-driven operator fallback."
},
{
"company": "AWS",
"role": "Senior Software Engineer (Step Functions)",
"period": "2022 — 2024",
"description": "Core contributor to AWS Step Functions, scaling to 57+ regions. Designed and implemented 8 critical components for distributed state machine execution. Achieved sub-100ms P99 latency for high-throughput workflow orchestration."
},
{
"company": "Titus",
"role": "ML Software Engineer Intern",
"period": "May — Aug 2019",
"description": "Built Personal Data Detection pipeline with 97.8% accuracy using transformer models. Developed Golang connector reducing p99 latency by 28%. Created SmartRegex system accelerating model deployment by 5x."
},
{
"company": "NAV Canada",
"role": "Summer Student",
"period": "May — Aug 2018",
"description": "Developed CFPS Flight Planning application using Django and React. Implemented NOTAMJ polling system and integrated Sonar code coverage. Built FWGS weather briefing interface for pilots."
}
]
},
"terminal": {
"title": "Poimen (Agent Terminal)",
"button": "Ask Poimen",
"helpText": "type 'help' for commands"
},
"footer": {
"copyright": "© 2025 Rock Liang. Deployed on homelab Kubernetes cluster (3-node Talos).",
"github": "GitHub",
"email": "Email"
}
},
"zh": {
"header": {
"title": "梁伟哲",
"subtitle": "高级软件工程师",
"nav": {
"home": "首页",
"askPoimen": "问Poimen",
"skills": "技能",
"contact": "联系"
},
"experience": "经验",
"experienceValue": "6年+",
"education": "学历",
"educationValue": "渥太华大学硕士",
"educationDetails": {
"bachelor": {
"school": "渥太华大学",
"degree": "计算机科学学士",
"period": "2016 2019"
},
"master": {
"school": "渥太华大学",
"degree": "计算机科学硕士",
"period": "2019年9月 2021年11月"
}
},
"contact": {
"portfolio": "portfolio.riotpiao.com",
"github": "GitHub",
"privateGithub": "私人GitHub",
"linkedin": "领英",
"email": "邮箱",
"copy": "复制"
},
"skillsPlaceholder": "技能将显示在这里"
},
"hero": {
"title": "高级软件工程师",
"subtitle": "基础设施 × 后端 × LLM系统",
"description": "为大规模用户构建可观测、可扩展、容错的系统。",
"exploreMore": "了解更多",
"terminalHint": "按",
"terminalHintSuffix": "通过终端探索",
"roles": [
"基础设施",
"SRE",
"软件开发",
"AI代理工程师",
"分布式系统",
"系统/平台工程师"
]
},
"bio": {
"title": "关于我",
"intro": "拥有6年以上经验的高级软件工程师,专注于为大规模用户构建可观测、可扩展、容错的系统。",
"expertise": {
"title": "技术专长",
"content": "基础设施自动化 (Terraform, Kubernetes, ArgoCD) • 分布式系统 (gRPC, Kafka, AWS Step Functions) • LLM推理优化 (INT4/INT8量化) • 后端系统 (Go, Java, Python, C++)"
},
"highlights": {
"title": "职业亮点",
"items": [
"在57+区域发布AWS Distributed-Map服务",
"在RBC构建多区域基础设施(成本降低40%)",
"架构生产级家庭实验室:Kubernetes、Kafka、LLM推理",
"开源项目:go-flink(分布式数据湖仓库)"
]
},
"educationTitle": "教育背景",
"educationContent": "渥太华大学计算机科学硕士 (2019–2021)\n渥太华大学计算机科学学士 (20162019)",
"currentFocus": "目前专注于基础设施自动化、LLM系统优化,以及构建下一代分布式系统。"
},
"projects": {
"title": "我构建的项目",
"subtitle": "展示我的作品",
"watchVideo": "观看视频",
"readArticle": "阅读文章",
"askPoimen": "问Poimen了解技术细节",
"items": [
{
"title": "AWS Distributed-Map",
"description": "发布面向关键任务的分布式任务编排服务。",
"longDescription": "在57+区域优化执行,具有容错调度和自动扩展功能。",
"stat": "生产发布,57+区域"
},
{
"title": "RBC: 多云平台",
"description": "统一基础设施平台,整合公有云和本地部署。",
"longDescription": "Terraform自动化配合Temporal编排。99.2%自动化配置,通知驱动的运维人员兜底。",
"stat": "200+微服务,4个区域,部署2小时→20分钟",
"highlight": "标准化12个团队的IaC模式——减少配置负担,集成速度提升3倍。",
"bullets": [
"集成Terraform Cloud实现IaC工作流集中化——12个团队onboarding时间缩短",
"构建K8s CronJob自动检测和修复Terraform状态漂移(Golang",
"设计Slack通知服务 + Golang worker——运维人员<5分钟解决apply失败",
"主导4个平台团队需求收集——澄清规格,解锁3个停滞项目",
"向非技术stakeholder翻译技术决策——获得平台迁移支持"
],
"deepDive": {
"label": "深入了解:Terraform状态漂移解决方案 →",
"url": "/terraform-drift"
}
},
{
"title": "家庭实验室:Kubernetes集群",
"description": "运行生产工作负载的裸机Kubernetes集群。",
"longDescription": "Talos OS 3节点控制平面,Cilium eBPF CNILonghorn存储,ArgoCD GitOps。",
"stat": "3节点集群,99.2%可用性"
},
{
"title": "家庭实验室:LLM推理",
"description": "具有CPU优化的生产LLM推理管道。",
"longDescription": "基于Temporal的编排,INT4/INT8量化,延迟降低60%。",
"stat": "延迟降低60%,实时推理"
},
{
"title": "家庭实验室:Kafka集群",
"description": "使用Strimzi KRaft的高吞吐量事件流。",
"longDescription": "3代理KRaft集群,具有自动扩展、持久存储和监控。",
"stat": "3代理,1K+消息/秒"
},
{
"title": "开源:go-flink",
"description": "用Go编写的分布式数据湖仓库框架。",
"longDescription": "具有流语义和高效处理的容错数据管道。",
"stat": "公开仓库,活跃中"
}
]
},
"experience": {
"title": "工作经历",
"subtitle": "欢迎向Poimen了解更多",
"items": [
{
"company": "riotpiao.com",
"role": "DevOps / SRE / 软件开发",
"period": "2026年5月 — 至今",
"description": "在裸机Kubernetes3节点Talos集群)上构建和维护生产级家庭实验室。架构基于Temporal的LLM推理管道,延迟降低60%。使用ArgoCD、Terraform和自定义操作器实现完整GitOps自动化。"
},
{
"company": "RBC",
"role": "技术主管",
"period": "2024年11月 — 2026年5月",
"description": "部署流程手动且缓慢——团队等待2+小时。选择Terraform处理幂等漂移,Temporal用于多云编排和内置重试。将部署时间缩短至20分钟,整合公有云和本地部署为统一平台,99.2%自动化,通知驱动的运维人员兜底机制。"
},
{
"company": "AWS",
"role": "高级软件工程师 (Step Functions)",
"period": "2022 — 2024",
"description": "AWS Step Functions核心贡献者,扩展至57+区域。设计实现8个关键组件用于分布式状态机执行。高吞吐量工作流编排实现P99延迟低于100ms。"
},
{
"company": "Titus",
"role": "机器学习软件工程师实习",
"period": "2019年5月 — 8月",
"description": "使用transformer模型构建准确率97.8%的个人数据检测管道。开发Golang连接器,p99延迟降低28%。创建SmartRegex系统,模型部署速度提升5倍。"
},
{
"company": "NAV Canada",
"role": "暑期实习生",
"period": "2018年5月 — 8月",
"description": "使用Django和React开发CFPS航班计划应用。实现NOTAMJ轮询系统并集成Sonar代码覆盖。为飞行员构建FWGS天气简报界面。"
}
]
},
"terminal": {
"title": "Poimen(代理终端)",
"button": "问Poimen",
"helpText": "输入 'help' 查看命令"
},
"footer": {
"copyright": "© 2025 梁伟哲。部署于家庭实验室Kubernetes集群(3节点Talos)。",
"github": "GitHub",
"email": "邮箱"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB