diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..69bc5d7 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.forgejo/workflows/build-push.yml b/.forgejo/workflows/build-push.yml new file mode 100644 index 0000000..5904eeb --- /dev/null +++ b/.forgejo/workflows/build-push.yml @@ -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 }}" diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..1db92b2 --- /dev/null +++ b/DESIGN.md @@ -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 +

{t.hero.title}

+``` + +### 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 `` 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). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..26aadee --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/app/layout.tsx b/app/layout.tsx index 6c51a91..625de0f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,7 @@ import './globals.css' import Header from '@/components/Header' +import { TerminalProvider } from '@/lib/TerminalContext' +import { LanguageProvider } from '@/lib/LanguageContext' export const metadata = { title: 'Rock Liang — Portfolio & Live Infrastructure', @@ -14,8 +16,12 @@ export default function RootLayout({ return ( -
- {children} + + +
+ {children} + + ); diff --git a/app/page.tsx b/app/page.tsx index 4f7d55a..e3b71e6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,71 +1,35 @@ 'use client' import { motion } from 'framer-motion' -import { FeatureCard } from '@/components/FeatureCard' +import { useState } from 'react' +import { X } from 'lucide-react' +import { ProjectShowcase } from '@/components/ProjectShowcase' import { HeroBlobFlow } from '@/components/HeroBlobFlow' import { InteractiveTerminal } from '@/components/InteractiveTerminal' import { ExperienceTimeline } from '@/components/ExperienceTimeline' -import { - GitBranch, - Database, - BarChart3, - MessageSquare, - Cpu, - Layers, -} from 'lucide-react' - -const features = [ - { - icon: GitBranch, - title: 'Infrastructure Platform', - description: 'Multi-region Terraform + Argo CD GitOps on Kubernetes', - href: '/infrastructure', - stat: '40% CPU reduction', - status: 'live' as const, - }, - { - icon: Cpu, - title: 'Distributed Systems', - description: 'gRPC, Kafka, AWS Step Functions across 57+ regions', - href: '/systems', - stat: 'Mission-critical', - status: 'live' as const, - }, - { - icon: MessageSquare, - title: 'LLM Systems', - description: 'CPU-bound inference optimization, INT4/INT8 quantization', - href: '/llm', - stat: '60% latency cut', - status: 'live' as const, - }, - { - icon: Database, - title: 'Kafka Cluster', - description: 'Strimzi KRaft cluster with auto-scaling brokers', - href: '/kafka', - stat: '3 brokers', - status: 'live' as const, - }, - { - icon: Layers, - title: 'Open Source', - description: 'go-flink: distributed DataLakeHouse in Go', - href: '/opensource', - stat: 'Public repo', - status: 'live' as const, - }, - { - icon: BarChart3, - title: 'Observability', - description: 'Prometheus, Grafana, Dynatrace — live metrics', - href: '/infrastructure', - stat: '99.2% uptime', - status: 'live' as const, - }, -] +import { useLanguage } from '@/lib/LanguageContext' export default function Home() { + const [bioModalOpen, setBioModalOpen] = useState(false) + const { t } = useLanguage() + + const projects = t.projects.items.map((item, index) => ({ + ...item, + id: index === 1 ? 'project-rbc' : undefined, + status: index === 1 + ? 'completed' as const + : index === 5 + ? 'building' as const + : 'live' as const, + media: index === 0 + ? { type: 'video' as const, url: 'https://www.youtube.com/watch?v=wdJ5DN15jus' } + : index === 1 + ? { type: 'image' as const, url: '/rbc-images.jpeg' } + : undefined, + videoUrl: index === 5 ? 'https://github.com/rockliang/go-flink' : '#', + articleUrl: index === 0 ? 'https://lnkd.in/p/gm2PZkWw' : '#', + })) + return (
{/* Hero */} @@ -82,30 +46,22 @@ export default function Home() { className="text-center mt-16" >

- Full-Stack Systems Engineer + {t.hero.title}

- Infrastructure × Backend × LLM Systems + {t.hero.subtitle}

- Building observable, fault-tolerant systems from bare metal to cloud. Optimizing cost (infrastructure) × performance (inference) × reliability (SRE). + {t.hero.description}

- 💡 Press Cmd+K to explore via terminal + 💡 {t.hero.terminalHint} Cmd+K {t.hero.terminalHintSuffix} @@ -132,9 +88,9 @@ export default function Home() { transition={{ duration: 0.6 }} className="mb-16" > -

What I Build

+

{t.projects.title}

- Production systems spanning infrastructure, distributed backends, and LLM optimization. Click any domain to explore. + {t.projects.subtitle}

@@ -147,14 +103,20 @@ export default function Home() { visible: { opacity: 1, transition: { - staggerChildren: 0.1, + staggerChildren: 0.15, }, }, }} - className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" + className="grid grid-cols-1 gap-8 max-w-3xl mx-auto" > - {features.map((feature) => ( - + {projects.map((project) => ( + ))} @@ -162,14 +124,14 @@ export default function Home() { {/* Footer */}
-

© 2025 Rock Liang. Deployed on homelab Kubernetes cluster (3-node Talos).

+

{t.footer.copyright}

- - GitHub + + {t.footer.github} {' • '} - Email + {t.footer.email}

@@ -177,6 +139,70 @@ export default function Home() { {/* Interactive Terminal */} + + {/* Bio Modal */} + {bioModalOpen && ( + setBioModalOpen(false)} + > + 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 */} +
+

{t.bio.title}

+ +
+ + {/* Content */} +
+

+ {t.bio.intro} +

+ +
+

{t.bio.expertise.title}

+

+ {t.bio.expertise.content} +

+
+ +
+

{t.bio.highlights.title}

+
    + {t.bio.highlights.items.map((item, i) => ( +
  • • {item}
  • + ))} +
+
+ +
+

{t.bio.educationTitle}

+

+ {t.bio.educationContent} +

+
+ +

+ {t.bio.currentFocus} +

+
+
+
+ )}
) } diff --git a/app/terraform-drift/page.tsx b/app/terraform-drift/page.tsx new file mode 100644 index 0000000..256f8b3 --- /dev/null +++ b/app/terraform-drift/page.tsx @@ -0,0 +1,51 @@ +'use client' + +import Link from 'next/link' +import { ArrowLeft } from 'lucide-react' + +export default function TerraformDriftPage() { + return ( +
+
+ + + Back to Projects + + +

+ Terraform State Drift Solution +

+ +

+ Coming soon... +

+ + {/* Placeholder sections */} +
+
+

The Problem

+

Content coming soon...

+
+ +
+

Immediate Fix

+

Content coming soon...

+
+ +
+

Short Term Fix

+

Content coming soon...

+
+ +
+

Long Term Fix

+

Content coming soon...

+
+
+
+
+ ) +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..007750e --- /dev/null +++ b/build.sh @@ -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}" diff --git a/components/ExperienceTimeline.tsx b/components/ExperienceTimeline.tsx index 500806a..cf29081 100644 --- a/components/ExperienceTimeline.tsx +++ b/components/ExperienceTimeline.tsx @@ -1,54 +1,38 @@ 'use client' import { motion } from 'framer-motion' +import { useLanguage } from '@/lib/LanguageContext' -interface TimelineItem { - company: string - role: string - period: string - description: string - impact: string - skills: string[] - color: string -} +const colors = [ + 'from-green-500 to-green-600', + 'from-red-500 to-red-600', + 'from-orange-500 to-orange-600', + 'from-cyan-500 to-cyan-600', + 'from-blue-500 to-blue-600', +] -const timeline: TimelineItem[] = [ - { - company: 'RBC', - role: 'Infrastructure Platform Architect', - period: '2020 — Present', - description: 'Leading multi-region infrastructure platform on Kubernetes. Building GitOps workflows with Terraform and Argo CD across 4 global regions.', - impact: '40% CPU reduction, 99.2% uptime', - skills: ['Kubernetes', 'Terraform', 'Argo CD', 'Go', 'AWS', 'Cilium', 'Prometheus', 'Grafana'], - color: 'from-blue-500 to-blue-600', - }, - { - company: 'AWS', - role: 'Senior Software Engineer (Step Functions)', - period: '2019 — 2020', - description: 'Designed and optimized distributed orchestration platform. Built fault-tolerant task scheduling across 57+ regions serving mission-critical workloads.', - impact: '8 core components, 57+ regions', - skills: ['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'], - color: 'from-orange-500 to-orange-600', - }, - { - company: 'Homelab', - role: 'DevOps / SRE / SDE', - period: '2021 — Present', - description: 'Production-grade bare-metal K8s cluster (Talos + Cilium). Built Temporal-based LLM inference pipeline with CPU-bound optimization and quantization.', - impact: '60% latency cut, 75% memory saved', - skills: ['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability'], - color: 'from-green-500 to-green-600', - }, - { - company: 'Open Source', - role: 'Maintainer — go-flink', - period: '2022 — Present', - description: 'Building distributed DataLakeHouse framework in Go. Fault-tolerant task scheduling, streaming semantics, and efficient data processing.', - impact: 'Public repository, active development', - skills: ['Go', 'Distributed Systems', 'Data Pipeline', 'Testing', 'Architecture'], - color: 'from-purple-500 to-purple-600', - }, +const skills = [ + ['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability', 'Go-Flink'], + ['Golang', 'Terraform', 'IaC', 'OpenShift', 'Docker', 'Distributed Systems', 'Grafana'], + ['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'], + ['Machine Learning', 'Golang', 'Anomaly Detection', 'C++', 'CMake', 'Data Security'], + ['Django', 'React', 'Agile', 'Web Development', 'Sonar', 'ATC Systems'], +] + +const links = [ + null, + '#project-rbc', // RBC - link to RBC project showcase + null, + null, + 'https://plan.navcanada.ca/account/login/?next=/', +] + +const linkTexts = [ + '', + 'Learn more →', + '', + '', + 'View Product →', ] const containerVariants = { @@ -71,6 +55,9 @@ const itemVariants = { } export function ExperienceTimeline() { + const { t } = useLanguage() + const timeline = t.experience.items + return (
-

Explore Experience

+

{t.experience.title}

- From AWS to RBC. Building systems at scale. Skills acquired along the journey. + {t.experience.subtitle}

@@ -105,7 +92,7 @@ export function ExperienceTimeline() { className="relative pl-8 md:pl-0" > {/* Timeline dot */} -
+
{/* Content card */}
@@ -128,17 +115,24 @@ export function ExperienceTimeline() { {item.description}

-
- 📊 {item.impact} -
- -
- {item.skills.map((skill) => ( +
+ {skills[index]?.map((skill) => ( {skill} ))}
+ + {links[index] && ( + + {linkTexts[index] || 'View Product →'} + + )}
diff --git a/components/Header.tsx b/components/Header.tsx index 6b1e0db..d172eff 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,12 +1,31 @@ 'use client' import Link from 'next/link' +import Image from 'next/image' import { Menu, X, Moon, Sun } from 'lucide-react' import { useState, useEffect } from 'react' +import { useTerminal } from '@/lib/TerminalContext' +import { useLanguage } from '@/lib/LanguageContext' export default function Header() { const [open, setOpen] = useState(false) const [dark, setDark] = useState(false) + const [skillsOpen, setSkillsOpen] = useState(false) + const [contactOpen, setContactOpen] = useState(false) + const [educationOpen, setEducationOpen] = useState(false) + const { isOpen: terminalOpen, setIsOpen: setTerminalOpen } = useTerminal() + const { lang, setLang, t } = useLanguage() + const [copied, setCopied] = useState(null) + + const handleCopy = (text: string, label: string) => { + navigator.clipboard.writeText(text) + setCopied(label) + setTimeout(() => setCopied(null), 2000) + } + + const toggleLang = () => { + setLang(lang === 'en' ? 'zh' : 'en') + } useEffect(() => { const isDark = document.documentElement.classList.contains('dark') @@ -22,42 +41,315 @@ export default function Header() {
-
- RL -
+ Profile
-
Rock Liang
-
Software Engineer Lead
+
{t.header.title}
+
{t.header.subtitle}
- +
+ {/* Mobile Language Toggle */} + + +
{open && ( -
- Home - Infrastructure - Kafka - CI/CD - Agent +
+ {t.header.nav.home} + + + {skillsOpen && ( +
+
{t.header.skillsPlaceholder}
+
+ )} + + {contactOpen && ( +
+
+ + {t.header.contact.portfolio} + + +
+
+ + {t.header.contact.github} + + +
+
+ + {t.header.contact.privateGithub} + + +
+
+ + {t.header.contact.linkedin} + + +
+
+ + {t.header.contact.email} + + +
+
+ )} +
+
+
{t.header.experience}
+
{t.header.experienceValue}
+
+
+ + {educationOpen && ( +
+
+

{t.header.educationDetails.bachelor.school}

+

{t.header.educationDetails.bachelor.degree}

+

{t.header.educationDetails.bachelor.period}

+
+
+

{t.header.educationDetails.master.school}

+

{t.header.educationDetails.master.degree}

+

{t.header.educationDetails.master.period}

+
+
+ )} +
+
)}
diff --git a/components/HeroBlobFlow.tsx b/components/HeroBlobFlow.tsx index 0a5dfdb..35341d8 100644 --- a/components/HeroBlobFlow.tsx +++ b/components/HeroBlobFlow.tsx @@ -2,78 +2,92 @@ import { motion, AnimatePresence } from 'framer-motion' import { useState, useEffect } from 'react' -import { Code, Network, Zap, Sparkles, Github } from 'lucide-react' -import { TypewriterText } from './TypewriterText' +import Image from 'next/image' +import { Code, Network, Zap, Sparkles, Shield, Cpu } from 'lucide-react' +import { useLanguage } from '@/lib/LanguageContext' -const roles = [ - { text: 'Software Engineer Lead', icon: Code }, - { text: 'Infrastructure Architect', icon: Network }, - { text: 'Distributed Systems Builder', icon: Zap }, - { text: 'LLM Systems Optimizer', icon: Sparkles }, - { text: 'Open Source Developer', icon: Github }, -] +const roleIcons = [Network, Shield, Code, Sparkles, Zap, Cpu] export function HeroBlobFlow() { const [index, setIndex] = useState(0) + const [isDark, setIsDark] = useState(false) + const { t } = useLanguage() + const roles = t.hero.roles + + useEffect(() => { + const isDarkMode = document.documentElement.classList.contains('dark') + setIsDark(isDarkMode) + + const observer = new MutationObserver(() => { + const darkMode = document.documentElement.classList.contains('dark') + setIsDark(darkMode) + }) + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }) + + return () => observer.disconnect() + }, []) useEffect(() => { const interval = setInterval(() => { setIndex((prev) => (prev + 1) % roles.length) }, 2000) return () => clearInterval(interval) - }, []) + }, [roles.length]) return ( -
- {/* Background glow */} -
- + {/* Full hire-me image */} +
+ You hire me, you get a bundle -
- - {/* Avatar center */} - -
- RL -
-
- - {/* Typewriter text below avatar */} - - - {/* Rotating role text */} -
- + + {/* Avatar + Rotating text - centered on image */} +
+ {/* Avatar */} - {(() => { - const Icon = roles[index].icon - return - })()} - {roles[index].text} + Profile - + + {/* Rotating role text */} +
+ + + {(() => { + const Icon = roleIcons[index] + return + })()} + {roles[index]} + + +
+
) diff --git a/components/InteractiveTerminal.tsx b/components/InteractiveTerminal.tsx index b6d8c8e..eb0bbb8 100644 --- a/components/InteractiveTerminal.tsx +++ b/components/InteractiveTerminal.tsx @@ -2,6 +2,7 @@ import { motion } from 'framer-motion' import { useEffect, useRef, useState } from 'react' +import { useTerminal } from '@/lib/TerminalContext' const commands: Record = { help: `Available commands: @@ -46,10 +47,15 @@ Open Source: go-flink (distributed DataLakeHouse)`, export function InteractiveTerminal() { const [input, setInput] = useState('') const [history, setHistory] = useState<{ cmd: string; output: string }[]>([]) - const [isOpen, setIsOpen] = useState(false) + const { isOpen, setIsOpen } = useTerminal() + const [isMac, setIsMac] = useState(true) const terminalRef = useRef(null) useEffect(() => { + // Detect if user is on Mac + const isMacOS = /Mac|iPhone|iPad|iPod/.test(navigator.platform) + setIsMac(isMacOS) + const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() @@ -59,7 +65,7 @@ export function InteractiveTerminal() { window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [isOpen]) + }, [isOpen, setIsOpen]) const handleCommand = (cmd: string) => { const trimmed = cmd.trim().toLowerCase() @@ -89,7 +95,7 @@ export function InteractiveTerminal() { onClick={() => setIsOpen(true)} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-mono text-sm shadow-lg" > - Terminal (Cmd+K) + Ask Poimen ({isMac ? 'Cmd' : 'Ctrl'}+K) ) : (
- ZSH_THEME="robbyrussell" + Poimen (Agent Terminal)