# Portfolio Tết Theme - Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development OR superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. **Goal:** Build a fully polished, production-ready portfolio with Next.js 14, Tailwind, Framer Motion, and Tết theme. **Architecture:** Next.js App Router with TypeScript, components in `components/`, data in `data/`, effects in `components/TetEffects.tsx`. Single-page Home with scroll sections; separate pages for Projects and My Corner blog. **Tech Stack:** Next.js 14+, TypeScript, Tailwind CSS, Framer Motion, Google Fonts. --- ### Task 1: Initialize Next.js project **Files:** - Create: `package.json` - Create: `next.config.js` - Create: `tsconfig.json` - Create: `tailwind.config.ts` - Create: `postcss.config.mjs` - Create: `app/layout.tsx` - Create: `app/page.tsx` - Create: `app/globals.css` - [ ] **Step 1:** Run create-next-app with TypeScript and Tailwind ```bash cd /home/node/.openclaw/workspace/projects/clawteam-portfolio npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --no-turbopack --import-alias "@/*" ``` Accept defaults. This creates the base structure. - [ ] **Step 2:** Install additional dependencies ```bash npm install framer-motion npm install -D @types/node ``` - [ ] **Step 3:** Configure Tailwind colors for Tết theme Edit `tailwind.config.ts`: ```ts import type { Config } from "tailwindcss"; const config: Config = { content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { colors: { primary: { 50: '#FFEBEE', 100: '#FFCDD2', 200: '#EF9A9A', 300: '#E57373', 400: '#EF5350', 500: '#F44336', 600: '#D32F2F', 700: '#C62828', 800: '#B71C1C', 900: '#FFCDD2', }, accent: { 50: '#FFF8E1', 100: '#FFECB3', 200: '#FFE082', 300: '#FFD54F', 400: '#FFCA28', 500: '#FFC107', 600: '#FFB300', 700: '#FFA000', 800: '#FF8F00', 900: '#FF6F00', }, }, fontFamily: { heading: ['"Playfair Display"', 'serif'], body: ['"Inter"', 'sans-serif'], }, }, }, plugins: [], }; export default config; ``` - [ ] **Step 4:** Load Google Fonts in `app/layout.tsx` Edit `app/layout.tsx`: ```tsx import type { Metadata } from "next"; import { Inter, Playfair_Display } from "next/font/google"; import "./globals.css"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); const playfair = Playfair_Display({ subsets: ["latin"], variable: "--font-playfair", }); export const metadata: Metadata = { title: "Trí Vĩ | AI Engineer", description: "Personal portfolio of Nguyễn Ngọc Trí Vĩ - AI Engineer", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ``` - [ ] **Step 5:** Set base styles in `app/globals.css` ```css @tailwind base; @tailwind components; @tailwind utilities; :root { --font-inter: var(--font-inter); --font-playfair: var(--font-playfair); } body { @apply bg-white text-gray-900; } h1, h2, h3, h4, h5, h6 { @apply font-heading; } ``` - [ ] **Step 6:** Commit initial setup ```bash git add . git commit -m "feat: init Next.js project with Tailwind and fonts" ``` --- ### Task 2: Create data layer **Files:** - Create: `src/data/profile.ts` - Create: `src/data/experience.ts` - Create: `src/data/projects.ts` - Create: `src/data/blogPosts.ts` - [ ] **Step 1:** Copy avatar image to `public/avatar.jpeg` (already exists in projects dir) ```bash cp /home/node/.openclaw/workspace/projects/clawteam-portfolio/avatar.jpeg public/avatar.jpeg ``` - [ ] **Step 2:** Create `src/data/profile.ts` ```ts export const profile = { name: "Nguyễn Ngọc Trí Vĩ", title: "AI Engineer", avatar: "/avatar.jpeg", email: "nntrivi2001@gmail.com", phone: "090 321 5095", linkedin: "https://linkedin.com/in/nguyen-ngoc-tri-vi", portfolio: "https://nguyenngoctrivi.vercel.app", languages: ["Vietnamese (native)", "English (intermediate)"], bio: `I'm an AI Engineer passionate about applying cutting-edge machine learning to real-world problems. With a strong foundation in Computer Vision and AI research, I've built solutions for industrial quality inspection, trading automation, and educational AI tools. I love exploring new technologies and turning complex ideas into practical, impactful products.`, }; ``` - [ ] **Step 3:** Create `src/data/experience.ts` ```ts export interface Experience { id: number; company: string; role: string; startDate: string; endDate: string; description: string[]; } export const experience: Experience[] = [ { id: 1, company: "SmartGift Solution", role: "AI Engineer", startDate: "Mar 2026", endDate: "Present", description: [ "Research and deploy AI assistants to improve development team productivity.", "Explore and apply suitable technologies for business problems, build POCs, and document practical recommendations.", ], }, { id: 2, company: "Learning Chain Vietnam", role: "Applied AI Researcher & Operations Associate", startDate: "Jul 2025", endDate: "Mar 2026", description: [ "Research and implement AI for trading automation and operations support.", "Explore and execute suitable tech, build POCs and document actionable recommendations.", "Design and teach applied AI training programs, workshops, and n8n templates.", "Support COO in optimizing workflows, reporting, and cross-team process standardization.", ], }, { id: 3, company: "Vườn đậu - Thương hiệu Sữa đậu nành Thảo mộc", role: "Founder", startDate: "Oct 2024", endDate: "Apr 2025", description: [ "Managed entire production and business process: sourcing, manufacturing, packaging, distribution, and inventory control.", "Planned raw material procurement, production schedules, and warehousing to optimize costs and maintain product quality.", "Developed pricing strategy and direct sales channels. Achieved strong customer satisfaction with average repurchase rate of 2.51x per customer.", ], }, { id: 4, company: "VSTECH Company Limited", role: "Computer Vision Engineer", startDate: "Aug 2023", endDate: "Feb 2025", description: [ "Nozzle Quality Inspection: Built for Japanese client on production line.", "3D Vision Integration: Measured Samsung LED panel height with ±7% accuracy.", "OCR Solutions: Read package codes and text labels with high accuracy in industrial environment.", ], }, ]; ``` - [ ] **Step 4:** Create `src/data/projects.ts` ```ts export interface Project { id: number; title: string; description: string; tech: string[]; demoUrl?: string; githubUrl?: string; image?: string; } export const projects: Project[] = [ { id: 1, title: "Nozzle Quality Inspection System", description: "Computer vision system for automatic nozzle defect detection on production lines. Deployed at Japanese manufacturing facility.", tech: ["Python", "OpenCV", "TensorFlow", "Industrial Camera"], demoUrl: "https://nguyenngoctrivi.vercel.app", githubUrl: "https://github.com/yourusername/nozzle-inspection", }, { id: 2, title: "3D Panel Measurement", description: "3D vision integration to measure LED panel height with ±7% accuracy for Samsung.", tech: ["3D Vision", "Python", "C++", "Point Cloud Processing"], }, { id: 3, title: "OCR for Package Codes", description: "High-accuracy OCR for reading package codes and text labels in industrial environments.", tech: ["OCR", "Python", "OpenCV", "Deep Learning"], }, { id: 4, title: "AI Trading Assistant", description: "Research and implementation of AI-driven trading automation and operations support.", tech: ["Python", "n8n", "PineScript", "Trading APIs"], }, { id: 5, title: "Applied AI Training Program", description: "Designed and delivered applied AI training courses, workshops, and learning assets (n8n templates).", tech: ["AI Education", "n8n", "Workshop Design"], }, ]; ``` - [ ] **Step 5:** Create `src/data/blogPosts.ts` ```ts export interface BlogPost { slug: string; title: string; date: string; excerpt: string; content: string; image?: string; tags: string[]; } export const blogPosts: BlogPost[] = [ { slug: "journey-into-ai", title: "My Journey into AI", date: "2025-12-15", excerpt: "Reflecting on how I got started with AI, from university days to real-world deployments.", content: ` # My Journey into AI It all started at university... ## The Beginning I was fascinated by how machines could learn... ## First Project My first computer vision project taught me... ## Lessons Learned - Start with the fundamentals - Build real things - Never stop learning This journey has been incredible, and I'm excited for what's next. `.trim(), tags: ["personal", "ai", "career"], }, { slug: "tet-vibes-2025", title: "Tết 2025 - A Time of Renewal", date: "2025-01-28", excerpt: "Celebrating Lunar New Year with family, food, and wishes for the year ahead.", content: ` # Tết 2025 - A Time of Renewal Tết is always a special time... ## Family Reunion Returning home, sharing meals... ## Traditions Visiting relatives, lucky money... ## Looking Forward New year, new goals, new hope. Chúc mừng năm mới! 🎊 `.trim(), image: "/images/tet-2025.jpg", tags: ["personal", "tet", "life"], }, ]; ``` - [ ] **Step 6:** Commit data layer ```bash git add src/data/ public/avatar.jpeg git commit -m "feat: add data layer (profile, experience, projects, blog)" ``` --- ### Task 3: Build TetEffects component (falling petals + subtle fireworks) **Files:** - Create: `src/components/TetEffects.tsx` - [ ] **Step 1:** Create falling petals canvas component ```tsx "use client"; import { useEffect, useRef } from "react"; interface Petal { x: number; y: number; size: number; speedY: number; speedX: number; rotation: number; rotationSpeed: number; opacity: number; } export default function TetEffects() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; resize(); window.addEventListener("resize", resize); const petals: Petal[] = []; const colors = ["#F44336", "#E91E63", "#D32F2F", "#FFC107", "#FFEB3B"]; // reds and golds const createPetal = () => ({ x: Math.random() * canvas.width, y: -10, size: 4 + Math.random() * 8, speedY: 0.5 + Math.random() * 1.5, speedX: (Math.random() - 0.5) * 0.5, rotation: Math.random() * Math.PI * 2, rotationSpeed: (Math.random() - 0.5) * 0.02, opacity: 0.4 + Math.random() * 0.4, }); // Initial petals for (let i = 0; i < 50; i++) { const p = createPetal(); p.y = Math.random() * canvas.height; petals.push(p); } let animationFrameId: number; const animate = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); petals.forEach((petal, idx) => { // Update petal.y += petal.speedY; petal.x += petal.speedX; petal.rotation += petal.rotationSpeed; // Reset if out of bounds if (petal.y > canvas.height + 10) { petals[idx] = createPetal(); } // Draw petal (simple circle or ellipse) ctx.save(); ctx.translate(petal.x, petal.y); ctx.rotate(petal.rotation); ctx.globalAlpha = petal.opacity; ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)]; ctx.beginPath(); ctx.ellipse(0, 0, petal.size, petal.size / 2, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }); animationFrameId = requestAnimationFrame(animate); }; animate(); return () => { window.removeEventListener("resize", resize); cancelAnimationFrame(animationFrameId); }; }, []); return ( ); } ``` - [ ] **Step 2:** Add subtle fireworks (sparkles) on idle/scroll Enhance the same canvas in `TetEffects`: ```tsx // Inside useEffect after petal animation: const sparkles: { x: number; y: number; life: number; maxLife: number; size: number }[] = []; const createSparkle = (x?: number, y?: number) => ({ x: x ?? Math.random() * canvas.width, y: y ?? Math.random() * canvas.height, life: 1, maxLife: 60 + Math.random() * 60, size: 1 + Math.random() * 2, }); // Occasionally create sparkles setInterval(() => { if (sparkles.length < 20) { sparkles.push(createSparkle()); } }, 300); // In animate loop, after petals: sparkles.forEach((sparkle, idx) => { sparkle.life--; if (sparkle.life <= 0) { sparkles.splice(idx, 1); return; } ctx.save(); ctx.globalAlpha = sparkle.life / sparkle.maxLife; ctx.fillStyle = "#FFC107"; ctx.beginPath(); ctx.arc(sparkle.x, sparkle.y, sparkle.size, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }); ``` (Integrate into the existing animation loop.) - [ ] **Step 3:** Commit TetEffects ```bash git add src/components/TetEffects.tsx git commit -m "feat: add TetEffects component (falling petals + fireworks)" ``` --- ### Task 4: Build common UI components **Files:** - Create: `src/components/Header.tsx` - Create: `src/components/Footer.tsx` - Create: `src/components/ui/Button.tsx` (optional) - [ ] **Step 1:** Create `src/components/Header.tsx` ```tsx "use client"; import { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import Link from "next/link"; export default function Header() { const [scrolled, setScrolled] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 50); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []); const navLinks = [ { href: "#home", label: "Home" }, { href: "#about", label: "About" }, { href: "#experience", label: "Experience" }, { href: "#contact", label: "Contact" }, { href: "/projects", label: "Projects" }, { href: "/my-corner", label: "My Corner" }, ]; return (
{/* Logo */} Trí Vĩ {/* Desktop Nav */} {/* Mobile menu button */}
{/* Mobile Nav */} {mobileMenuOpen && (
{navLinks.map((link) => ( setMobileMenuOpen(false)} > {link.label} ))}
)}
); } ``` - [ ] **Step 2:** Create `src/components/Footer.tsx` ```tsx import Link from "next/link"; export default function Footer() { return (

Trí Vĩ

AI Engineer

© 2025 - 2026 Nguyễn Ngọc Trí Vĩ. All rights reserved.
{/* Decorative lion/dragon silhouette could be added here */}
); } ``` - [ ] **Step 3:** Commit Header & Footer ```bash git add src/components/Header.tsx src/components/Footer.tsx git commit -m "feat: add Header and Footer components" ``` --- ### Task 5: Build Hero section **Files:** - Create: `src/components/Hero.tsx` - [ ] **Step 1:** Create Hero with avatar, name, title, CTA buttons ```tsx "use client"; import { motion } from "framer-motion"; import Image from "next/image"; import { profile } from "@/data/profile"; export default function Hero() { return (
{/* Falling petals overlay will be rendered at app level */}
{profile.name}
{profile.name} {profile.title} View Projects Contact Me
); } ``` - [ ] **Step 2:** Add Avatar image to `public/` (already copied) - [ ] **Step 3:** Commit Hero ```bash git add src/components/Hero.tsx git commit -m "feat: add Hero section component" ``` --- ### Task 6: Build About section **Files:** - Create: `src/components/About.tsx` - [ ] **Step 1:** Create About component (2-column layout) ```tsx "use client"; import { motion } from "framer-motion"; import Image from "next/image"; import { profile } from "@/data/profile"; export default function About() { return (

About Me

{profile.name}

{profile.bio}

Languages

    {profile.languages.map((lang, idx) => (
  • {lang}
  • ))}

Contact

Email:{" "} {profile.email}

Phone: {profile.phone}

LinkedIn:{" "} {profile.linkedin}

); } ``` - [ ] **Step 2:** Commit About ```bash git add src/components/About.tsx git commit -m "feat: add About section component" ``` --- ### Task 7: Build Experience (Timeline) component **Files:** - Create: `src/components/Experience.tsx` - [ ] **Step 1:** Create Timeline with vertical line and nodes ```tsx "use client"; import { motion } from "framer-motion"; import { Experience } from "@/data/experience"; export default function Experience() { return (

Experience

{/* Vertical line */}
{experience.map((exp, index) => ( {/* Timeline node */}
{/* Content */}

{exp.role}

{exp.company}

{exp.startDate} - {exp.endDate}

    {exp.description.map((item, i) => (
  • {item}
  • ))}
))}
); } ``` - [ ] **Step 2:** Commit Experience ```bash git add src/components/Experience.tsx git commit -m "feat: add Experience timeline component" ``` --- ### Task 8: Build Contact section with form **Files:** - Create: `src/components/Contact.tsx` - [ ] **Step 1:** Create Contact form (no backend, just UI + success animation) ```tsx "use client"; import { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { profile } from "@/data/profile"; export default function Contact() { const [submitted, setSubmitted] = useState(false); const [formData, setFormData] = useState({ name: "", email: "", message: "", }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Simulate submission setSubmitted(true); setTimeout(() => { setSubmitted(false); setFormData({ name: "", email: "", message: "" }); }, 3000); }; return (

Get In Touch

{/* Contact Info */}

Contact Information

Email:
{profile.email}

Phone:
{profile.phone}

LinkedIn:
{profile.linkedin}

I'd love to hear from you! Whether you have a project in mind, want to collaborate, or just want to say hi, feel free to reach out.

{/* Form */}
setFormData({ ...formData, name: e.target.value }) } className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition" placeholder="Your name" />
setFormData({ ...formData, email: e.target.value }) } className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition" placeholder="your@email.com" />