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:
@@ -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
|
||||||
@@ -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 }}"
|
||||||
@@ -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
@@ -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"]
|
||||||
+8
-2
@@ -1,5 +1,7 @@
|
|||||||
import './globals.css'
|
import './globals.css'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
|
import { TerminalProvider } from '@/lib/TerminalContext'
|
||||||
|
import { LanguageProvider } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Rock Liang — Portfolio & Live Infrastructure',
|
title: 'Rock Liang — Portfolio & Live Infrastructure',
|
||||||
@@ -14,8 +16,12 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className="bg-white dark:bg-gray-950 text-gray-900 dark:text-white">
|
<body className="bg-white dark:bg-gray-950 text-gray-900 dark:text-white">
|
||||||
<Header />
|
<LanguageProvider>
|
||||||
{children}
|
<TerminalProvider>
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
</TerminalProvider>
|
||||||
|
</LanguageProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+112
-86
@@ -1,71 +1,35 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { motion } from 'framer-motion'
|
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 { HeroBlobFlow } from '@/components/HeroBlobFlow'
|
||||||
import { InteractiveTerminal } from '@/components/InteractiveTerminal'
|
import { InteractiveTerminal } from '@/components/InteractiveTerminal'
|
||||||
import { ExperienceTimeline } from '@/components/ExperienceTimeline'
|
import { ExperienceTimeline } from '@/components/ExperienceTimeline'
|
||||||
import {
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function Home() {
|
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 (
|
return (
|
||||||
<main className="min-h-screen">
|
<main className="min-h-screen">
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
@@ -82,30 +46,22 @@ export default function Home() {
|
|||||||
className="text-center mt-16"
|
className="text-center mt-16"
|
||||||
>
|
>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-2">
|
<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>
|
</h1>
|
||||||
<p className="text-xl text-gray-700 dark:text-gray-300 mb-6 font-medium">
|
<p className="text-xl text-gray-700 dark:text-gray-300 mb-6 font-medium">
|
||||||
Infrastructure × Backend × LLM Systems
|
{t.hero.subtitle}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-10">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div className="flex gap-4 justify-center flex-wrap mb-8">
|
<div className="flex gap-4 justify-center flex-wrap mb-8">
|
||||||
<a
|
<button
|
||||||
href="#features"
|
onClick={() => setBioModalOpen(true)}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
|
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
|
||||||
>
|
>
|
||||||
Explore Systems
|
{t.hero.exploreMore}
|
||||||
</a>
|
</button>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -114,7 +70,7 @@ export default function Home() {
|
|||||||
transition={{ duration: 0.8, delay: 1.2 }}
|
transition={{ duration: 0.8, delay: 1.2 }}
|
||||||
className="text-sm text-gray-600 dark:text-gray-400"
|
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>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,9 +88,9 @@ export default function Home() {
|
|||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="mb-16"
|
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">
|
<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>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -147,14 +103,20 @@ export default function Home() {
|
|||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
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) => (
|
{projects.map((project) => (
|
||||||
<FeatureCard key={feature.title} {...feature} />
|
<ProjectShowcase
|
||||||
|
key={project.title}
|
||||||
|
{...project}
|
||||||
|
watchVideoText={t.projects.watchVideo}
|
||||||
|
readArticleText={t.projects.readArticle}
|
||||||
|
askPoimenText={t.projects.askPoimen}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
@@ -162,14 +124,14 @@ export default function Home() {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 py-12 px-6">
|
<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">
|
<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">
|
<p className="mt-2">
|
||||||
<a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400">
|
<a href="https://github.com/rockliang" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400">
|
||||||
GitHub
|
{t.footer.github}
|
||||||
</a>
|
</a>
|
||||||
{' • '}
|
{' • '}
|
||||||
<a href="mailto:[email protected]" className="hover:text-blue-600 dark:hover:text-blue-400">
|
<a href="mailto:[email protected]" className="hover:text-blue-600 dark:hover:text-blue-400">
|
||||||
Email
|
{t.footer.email}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,6 +139,70 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Interactive Terminal */}
|
{/* Interactive Terminal */}
|
||||||
<InteractiveTerminal />
|
<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>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
@@ -1,54 +1,38 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
interface TimelineItem {
|
const colors = [
|
||||||
company: string
|
'from-green-500 to-green-600',
|
||||||
role: string
|
'from-red-500 to-red-600',
|
||||||
period: string
|
'from-orange-500 to-orange-600',
|
||||||
description: string
|
'from-cyan-500 to-cyan-600',
|
||||||
impact: string
|
'from-blue-500 to-blue-600',
|
||||||
skills: string[]
|
]
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline: TimelineItem[] = [
|
const skills = [
|
||||||
{
|
['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability', 'Go-Flink'],
|
||||||
company: 'RBC',
|
['Golang', 'Terraform', 'IaC', 'OpenShift', 'Docker', 'Distributed Systems', 'Grafana'],
|
||||||
role: 'Infrastructure Platform Architect',
|
['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'],
|
||||||
period: '2020 — Present',
|
['Machine Learning', 'Golang', 'Anomaly Detection', 'C++', 'CMake', 'Data Security'],
|
||||||
description: 'Leading multi-region infrastructure platform on Kubernetes. Building GitOps workflows with Terraform and Argo CD across 4 global regions.',
|
['Django', 'React', 'Agile', 'Web Development', 'Sonar', 'ATC Systems'],
|
||||||
impact: '40% CPU reduction, 99.2% uptime',
|
]
|
||||||
skills: ['Kubernetes', 'Terraform', 'Argo CD', 'Go', 'AWS', 'Cilium', 'Prometheus', 'Grafana'],
|
|
||||||
color: 'from-blue-500 to-blue-600',
|
const links = [
|
||||||
},
|
null,
|
||||||
{
|
'#project-rbc', // RBC - link to RBC project showcase
|
||||||
company: 'AWS',
|
null,
|
||||||
role: 'Senior Software Engineer (Step Functions)',
|
null,
|
||||||
period: '2019 — 2020',
|
'https://plan.navcanada.ca/account/login/?next=/',
|
||||||
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'],
|
const linkTexts = [
|
||||||
color: 'from-orange-500 to-orange-600',
|
'',
|
||||||
},
|
'Learn more →',
|
||||||
{
|
'',
|
||||||
company: 'Homelab',
|
'',
|
||||||
role: 'DevOps / SRE / SDE',
|
'View Product →',
|
||||||
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 containerVariants = {
|
const containerVariants = {
|
||||||
@@ -71,6 +55,9 @@ const itemVariants = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExperienceTimeline() {
|
export function ExperienceTimeline() {
|
||||||
|
const { t } = useLanguage()
|
||||||
|
const timeline = t.experience.items
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-4xl mx-auto px-6 py-20">
|
<section className="max-w-4xl mx-auto px-6 py-20">
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -80,9 +67,9 @@ export function ExperienceTimeline() {
|
|||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="mb-16"
|
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">
|
<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>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -105,7 +92,7 @@ export function ExperienceTimeline() {
|
|||||||
className="relative pl-8 md:pl-0"
|
className="relative pl-8 md:pl-0"
|
||||||
>
|
>
|
||||||
{/* Timeline dot */}
|
{/* 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 */}
|
{/* Content card */}
|
||||||
<div className={index % 2 === 0 ? 'md:w-1/2 md:pr-8' : 'md:w-1/2 md:ml-auto md:pl-8'}>
|
<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}
|
{item.description}
|
||||||
</p>
|
</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">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
📊 {item.impact}
|
{skills[index]?.map((skill) => (
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{item.skills.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">
|
<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}
|
{skill}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
+317
-25
@@ -1,12 +1,31 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
import { Menu, X, Moon, Sun } from 'lucide-react'
|
import { Menu, X, Moon, Sun } from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTerminal } from '@/lib/TerminalContext'
|
||||||
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [dark, setDark] = 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(() => {
|
useEffect(() => {
|
||||||
const isDark = document.documentElement.classList.contains('dark')
|
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">
|
<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">
|
<div className="max-w-6xl mx-auto px-6 py-4 flex justify-between items-center">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<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">
|
<Image
|
||||||
RL
|
src="/avatar.jpeg"
|
||||||
</div>
|
alt="Profile"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="rounded-full object-cover"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Rock Liang</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">Software Engineer Lead</div>
|
<div className="text-xs text-gray-600 dark:text-gray-400">{t.header.subtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden md:flex gap-8 items-center">
|
<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="/" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">{t.header.nav.home}</Link>
|
||||||
<Link href="/cluster" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Cluster</Link>
|
<button
|
||||||
<Link href="/infrastructure" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Infrastructure</Link>
|
onClick={() => setTerminalOpen(!terminalOpen)}
|
||||||
<Link href="/kafka" className="text-sm hover:text-blue-600 dark:hover:text-blue-400">Kafka</Link>
|
className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
<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>
|
{t.header.nav.askPoimen}
|
||||||
<button onClick={toggleDark} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded">
|
|
||||||
{dark ? <Sun size={20} /> : <Moon size={20} />}
|
|
||||||
</button>
|
</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>
|
</nav>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-2 md:hidden">
|
||||||
onClick={() => setOpen(!open)}
|
{/* Mobile Language Toggle */}
|
||||||
className="md:hidden p-2"
|
<button
|
||||||
>
|
onClick={toggleLang}
|
||||||
{open ? <X size={24} /> : <Menu size={24} />}
|
className="px-2 py-1 text-xs font-semibold rounded border border-gray-300 dark:border-gray-600"
|
||||||
</button>
|
>
|
||||||
|
{lang === 'en' ? '中文' : 'EN'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="p-2"
|
||||||
|
>
|
||||||
|
{open ? <X size={24} /> : <Menu size={24} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<div className="md:hidden border-t border-gray-200 dark:border-gray-800 p-4 space-y-2">
|
<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">Home</Link>
|
<Link href="/" className="block py-2 text-sm hover:text-blue-600">{t.header.nav.home}</Link>
|
||||||
<Link href="/infrastructure" className="block py-2 text-sm hover:text-blue-600">Infrastructure</Link>
|
<button
|
||||||
<Link href="/kafka" className="block py-2 text-sm hover:text-blue-600">Kafka</Link>
|
onClick={() => setTerminalOpen(!terminalOpen)}
|
||||||
<Link href="/cicd" className="block py-2 text-sm hover:text-blue-600">CI/CD</Link>
|
className="block py-2 text-sm hover:text-blue-600"
|
||||||
<Link href="/agent" className="block py-2 text-sm hover:text-blue-600">Agent</Link>
|
>
|
||||||
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+70
-56
@@ -2,78 +2,92 @@
|
|||||||
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Code, Network, Zap, Sparkles, Github } from 'lucide-react'
|
import Image from 'next/image'
|
||||||
import { TypewriterText } from './TypewriterText'
|
import { Code, Network, Zap, Sparkles, Shield, Cpu } from 'lucide-react'
|
||||||
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
const roles = [
|
const roleIcons = [Network, Shield, Code, Sparkles, Zap, Cpu]
|
||||||
{ 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 },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function HeroBlobFlow() {
|
export function HeroBlobFlow() {
|
||||||
const [index, setIndex] = useState(0)
|
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(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setIndex((prev) => (prev + 1) % roles.length)
|
setIndex((prev) => (prev + 1) % roles.length)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [roles.length])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full max-w-md mx-auto aspect-square flex flex-col items-center justify-center">
|
<div className="relative w-full max-w-5xl mx-auto">
|
||||||
{/* Background glow */}
|
{/* Full hire-me image */}
|
||||||
<div className="absolute inset-0">
|
<div className="relative w-full">
|
||||||
<motion.div
|
<Image
|
||||||
animate={{
|
src={isDark ? '/hire-me.png' : '/hire-me-light.png'}
|
||||||
scale: [1, 1.15, 1],
|
alt="You hire me, you get a bundle"
|
||||||
opacity: [0.25, 0.4, 0.25],
|
width={1200}
|
||||||
}}
|
height={800}
|
||||||
transition={{
|
className="w-full h-auto"
|
||||||
duration: 6,
|
priority
|
||||||
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>
|
|
||||||
|
{/* Avatar + Rotating text - centered on image */}
|
||||||
{/* Avatar center */}
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20 flex flex-col items-center">
|
||||||
<motion.div
|
{/* Avatar */}
|
||||||
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>
|
|
||||||
</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">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{(() => {
|
<Image
|
||||||
const Icon = roles[index].icon
|
src="/avatar.jpeg"
|
||||||
return <Icon size={28} className="text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
alt="Profile"
|
||||||
})()}
|
width={200}
|
||||||
{roles[index].text}
|
height={200}
|
||||||
|
className="w-48 h-48 rounded-full object-cover border-4 border-white dark:border-gray-950 shadow-xl"
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
|
||||||
|
{/* Rotating role text */}
|
||||||
|
<div className="mt-6 h-16 flex items-center justify-center">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
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' }}
|
||||||
|
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 = roleIcons[index]
|
||||||
|
return <Icon size={32} className="text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||||
|
})()}
|
||||||
|
{roles[index]}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTerminal } from '@/lib/TerminalContext'
|
||||||
|
|
||||||
const commands: Record<string, string> = {
|
const commands: Record<string, string> = {
|
||||||
help: `Available commands:
|
help: `Available commands:
|
||||||
@@ -46,10 +47,15 @@ Open Source: go-flink (distributed DataLakeHouse)`,
|
|||||||
export function InteractiveTerminal() {
|
export function InteractiveTerminal() {
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [history, setHistory] = useState<{ cmd: string; output: string }[]>([])
|
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)
|
const terminalRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Detect if user is on Mac
|
||||||
|
const isMacOS = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
|
||||||
|
setIsMac(isMacOS)
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -59,7 +65,7 @@ export function InteractiveTerminal() {
|
|||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [isOpen])
|
}, [isOpen, setIsOpen])
|
||||||
|
|
||||||
const handleCommand = (cmd: string) => {
|
const handleCommand = (cmd: string) => {
|
||||||
const trimmed = cmd.trim().toLowerCase()
|
const trimmed = cmd.trim().toLowerCase()
|
||||||
@@ -89,7 +95,7 @@ export function InteractiveTerminal() {
|
|||||||
onClick={() => setIsOpen(true)}
|
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"
|
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>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<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"
|
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">
|
<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
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="text-gray-400 hover:text-white"
|
className="text-gray-400 hover:text-white"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
|
repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: infra/portfolio/base
|
path: infra/portfolio/base
|
||||||
destination:
|
destination:
|
||||||
@@ -27,7 +27,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
|
repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: infra/homarr/base
|
path: infra/homarr/base
|
||||||
destination:
|
destination:
|
||||||
@@ -48,7 +48,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
|
repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: infra/auth-infra/base
|
path: infra/auth-infra/base
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ metadata:
|
|||||||
namespace: longhorn-system
|
namespace: longhorn-system
|
||||||
annotations:
|
annotations:
|
||||||
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
|
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
|
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
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- longhorn.riotpiao.homelab.com
|
- longhorn.riotpiao.com
|
||||||
secretName: longhorn-tls
|
secretName: longhorn-tls
|
||||||
rules:
|
rules:
|
||||||
- host: longhorn.riotpiao.homelab.com
|
- host: longhorn.riotpiao.com
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
@@ -33,17 +33,17 @@ metadata:
|
|||||||
namespace: dashboard
|
namespace: dashboard
|
||||||
annotations:
|
annotations:
|
||||||
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
|
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
|
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
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- portainer.riotpiao.homelab.com
|
- portainer.riotpiao.com
|
||||||
secretName: portainer-tls
|
secretName: portainer-tls
|
||||||
rules:
|
rules:
|
||||||
- host: portainer.riotpiao.homelab.com
|
- host: portainer.riotpiao.com
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
@@ -61,17 +61,17 @@ metadata:
|
|||||||
namespace: monitoring
|
namespace: monitoring
|
||||||
annotations:
|
annotations:
|
||||||
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
|
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
|
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
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- prometheus.riotpiao.homelab.com
|
- prometheus.riotpiao.com
|
||||||
secretName: prometheus-tls
|
secretName: prometheus-tls
|
||||||
rules:
|
rules:
|
||||||
- host: prometheus.riotpiao.homelab.com
|
- host: prometheus.riotpiao.com
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ spec:
|
|||||||
env:
|
env:
|
||||||
AUTH_PROVIDERS: "oidc"
|
AUTH_PROVIDERS: "oidc"
|
||||||
AUTH_OIDC_CLIENT_ID: "homarr"
|
AUTH_OIDC_CLIENT_ID: "homarr"
|
||||||
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.homelab.com/application/o/homarr/"
|
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.com/application/o/homarr/"
|
||||||
AUTH_OIDC_URI: "https://authentik.riotpiao.homelab.com/application/o/homarr/.well-known/openid-configuration"
|
AUTH_OIDC_URI: "https://authentik.riotpiao.com/application/o/homarr/.well-known/openid-configuration"
|
||||||
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
|
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ spec:
|
|||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- homarr.riotpiao.homelab.com
|
- homarr.riotpiao.com
|
||||||
secretName: homarr-tls
|
secretName: homarr-tls
|
||||||
rules:
|
rules:
|
||||||
- host: homarr.riotpiao.homelab.com
|
- host: homarr.riotpiao.com
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: portfolio
|
- name: portfolio
|
||||||
image: forgejo.riotpiao.homelab.com/rock/portfolio:latest
|
image: forgejo.riotpiao.com/rock/portfolio:latest
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
|
|||||||
@@ -3,16 +3,13 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: portfolio
|
name: portfolio
|
||||||
namespace: portfolio
|
namespace: portfolio
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- portfolio.riotpiao.homelab.com
|
- riotpiao.com
|
||||||
secretName: portfolio-tls
|
secretName: portfolio-tls
|
||||||
rules:
|
rules:
|
||||||
- host: portfolio.riotpiao.homelab.com
|
- host: riotpiao.com
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 (2019–2021)\nB.Sc. Computer Science, University of Ottawa (2016–2019)",
|
||||||
|
"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渥太华大学计算机科学学士 (2016–2019)",
|
||||||
|
"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 CNI,Longhorn存储,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": "在裸机Kubernetes(3节点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 |
Reference in New Issue
Block a user