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

- Add i18n support (EN/ZH) with language toggle
- Redesign header with education dropdown, contact copy buttons
- Add RBC project showcase with bullet points, skills highlighting
- Create terraform-drift placeholder page
- Add ProjectShowcase component with media toggle (image/video)
- Update experience timeline with action-driven descriptions
- Add Docker + Forgejo CI workflow for image builds
- Update K8s manifests for riotpiao.com ingress
- Add DESIGN.md documenting design system patterns
This commit is contained in:
Story Crater Bot
2026-08-31 13:59:46 -07:00
parent e404dae7c8
commit b9ae470bc1
26 changed files with 1724 additions and 250 deletions
+70 -56
View File
@@ -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 (
<div className="relative w-full max-w-md mx-auto aspect-square flex flex-col items-center justify-center">
{/* Background glow */}
<div className="absolute inset-0">
<motion.div
animate={{
scale: [1, 1.15, 1],
opacity: [0.25, 0.4, 0.25],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute inset-0 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-full blur-3xl"
<div className="relative w-full max-w-5xl mx-auto">
{/* Full hire-me image */}
<div className="relative w-full">
<Image
src={isDark ? '/hire-me.png' : '/hire-me-light.png'}
alt="You hire me, you get a bundle"
width={1200}
height={800}
className="w-full h-auto"
priority
/>
</div>
{/* Avatar center */}
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="relative z-10 w-32 h-32"
>
<div className="w-full h-full rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center text-white font-bold text-4xl border-4 border-white dark:border-gray-950 shadow-xl">
RL
</div>
</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">
{/* Avatar + Rotating text - centered on image */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20 flex flex-col items-center">
{/* Avatar */}
<motion.div
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="absolute text-2xl font-semibold text-gray-900 dark:text-white text-center px-6 flex items-center gap-3 justify-center"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
{(() => {
const Icon = roles[index].icon
return <Icon size={28} className="text-blue-600 dark:text-blue-400 flex-shrink-0" />
})()}
{roles[index].text}
<Image
src="/avatar.jpeg"
alt="Profile"
width={200}
height={200}
className="w-48 h-48 rounded-full object-cover border-4 border-white dark:border-gray-950 shadow-xl"
/>
</motion.div>
</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>
)