Tools: Building a Modern Portfolio with Tailwind CSS v4, React, and Vite

Tools: Building a Modern Portfolio with Tailwind CSS v4, React, and Vite

Source: Dev.to

πŸš€ Why This Stack? ## πŸ“‹ Prerequisites ## πŸ› οΈ Step-by-Step Implementation ## 1. Project Initialization ## 2. Configure Vite for Tailwind v4 ## 3. Set Up Global Styles ## 4. Theme Context for Dark/Light Mode ## 5. Key Components ## 6. Building the Main Pages ## 7. Main App Component ## πŸ”Œ Backend Integration ## ✨ Key Features Implemented ## πŸš€ Deployment ## Build for Production ## Deploy to Vercel (Recommended) ## Deploy to Netlify ## πŸ“ˆ SEO Optimization Tips ## 🎨 Customization Ideas ## πŸ“š Resources ## πŸ’‘ Pro Tips ## 🎯 Conclusion github : https://github.com/bkoimett/bkoimett-portofolio.git Looking to build a sleek, modern portfolio that stands out? In this comprehensive guide, I'll walk you through creating a professional portfolio from scratch using the latest Tailwind CSS v4, React, and Vite. We'll build a fully responsive site with dark mode support and backend integration. src/context/ThemeContext.jsx Theme Toggle Button - src/components/ThemeToggle.jsx Home Page - src/pages/Home.jsx Projects Page with Filtering - src/pages/Projects.jsx To make the portfolio dynamic, you can set up a simple Express backend: You've now built a modern, professional portfolio with cutting-edge technologies! This setup gives you: The best part? You can easily extend this foundation with additional features like a blog, case studies, or even an admin dashboard. Ready to make it your own? Fork the code, customize the content, and deploy your portfolio today! Found this guide helpful? Share it with your network! Have questions? Drop them in the comments below. Tags: #React #TailwindCSS #Vite #WebDevelopment #Portfolio #Frontend #JavaScript #DevOps This tutorial was originally published on Dev.to. Follow me for more web development content! github: https://github.com/bkoimett/bkoimett-portofolio.git linkedIn: https://www.linkedin.com/in/benjamin-koimett-699959366/ Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: # Create a new Vite project with React npm create vite@latest my-portfolio -- --template react cd my-portfolio # Install dependencies npm install tailwindcss @tailwindcss/vite @vitejs/plugin-react npm install react-router-dom axios # Start development server npm run dev Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Create a new Vite project with React npm create vite@latest my-portfolio -- --template react cd my-portfolio # Install dependencies npm install tailwindcss @tailwindcss/vite @vitejs/plugin-react npm install react-router-dom axios # Start development server npm run dev COMMAND_BLOCK: # Create a new Vite project with React npm create vite@latest my-portfolio -- --template react cd my-portfolio # Install dependencies npm install tailwindcss @tailwindcss/vite @vitejs/plugin-react npm install react-router-dom axios # Start development server npm run dev CODE_BLOCK: import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [ react(), tailwindcss(), ], }) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [ react(), tailwindcss(), ], }) CODE_BLOCK: import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [ react(), tailwindcss(), ], }) CODE_BLOCK: @import "tailwindcss"; /* Custom theme variables */ @theme { --font-sans: 'Inter', system-ui, -apple-system, sans-serif; } /* Dark mode support */ :root { color-scheme: light; } :root.dark { color-scheme: dark; } /* Smooth theme transitions */ * { transition-property: background-color, border-color, color, fill, stroke; transition-duration: 200ms; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @import "tailwindcss"; /* Custom theme variables */ @theme { --font-sans: 'Inter', system-ui, -apple-system, sans-serif; } /* Dark mode support */ :root { color-scheme: light; } :root.dark { color-scheme: dark; } /* Smooth theme transitions */ * { transition-property: background-color, border-color, color, fill, stroke; transition-duration: 200ms; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); CODE_BLOCK: @import "tailwindcss"; /* Custom theme variables */ @theme { --font-sans: 'Inter', system-ui, -apple-system, sans-serif; } /* Dark mode support */ :root { color-scheme: light; } :root.dark { color-scheme: dark; } /* Smooth theme transitions */ * { transition-property: background-color, border-color, color, fill, stroke; transition-duration: 200ms; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); COMMAND_BLOCK: import React, { createContext, useState, useEffect, useContext } from 'react'; const ThemeContext = createContext(); export const useTheme = () => { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }; export const ThemeProvider = ({ children }) => { const [isDark, setIsDark] = useState(() => { const savedTheme = localStorage.getItem('theme'); if (!savedTheme) { return window.matchMedia('(prefers-color-scheme: dark)').matches; } return savedTheme === 'dark'; }); useEffect(() => { if (isDark) { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); } }, [isDark]); const toggleTheme = () => setIsDark(!isDark); return ( <ThemeContext.Provider value={{ isDark, toggleTheme }}> {children} </ThemeContext.Provider> ); }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import React, { createContext, useState, useEffect, useContext } from 'react'; const ThemeContext = createContext(); export const useTheme = () => { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }; export const ThemeProvider = ({ children }) => { const [isDark, setIsDark] = useState(() => { const savedTheme = localStorage.getItem('theme'); if (!savedTheme) { return window.matchMedia('(prefers-color-scheme: dark)').matches; } return savedTheme === 'dark'; }); useEffect(() => { if (isDark) { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); } }, [isDark]); const toggleTheme = () => setIsDark(!isDark); return ( <ThemeContext.Provider value={{ isDark, toggleTheme }}> {children} </ThemeContext.Provider> ); }; COMMAND_BLOCK: import React, { createContext, useState, useEffect, useContext } from 'react'; const ThemeContext = createContext(); export const useTheme = () => { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }; export const ThemeProvider = ({ children }) => { const [isDark, setIsDark] = useState(() => { const savedTheme = localStorage.getItem('theme'); if (!savedTheme) { return window.matchMedia('(prefers-color-scheme: dark)').matches; } return savedTheme === 'dark'; }); useEffect(() => { if (isDark) { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); } }, [isDark]); const toggleTheme = () => setIsDark(!isDark); return ( <ThemeContext.Provider value={{ isDark, toggleTheme }}> {children} </ThemeContext.Provider> ); }; COMMAND_BLOCK: import React from 'react'; import { useTheme } from '../context/ThemeContext'; const ThemeToggle = () => { const { isDark, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme} className="fixed top-6 right-6 p-3 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors z-50 shadow-md" aria-label="Toggle theme" > {isDark ? ( <svg className="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" /> </svg> ) : ( <svg className="w-5 h-5 text-gray-700" fill="currentColor" viewBox="0 0 20 20"> <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> </svg> )} </button> ); }; export default ThemeToggle; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import React from 'react'; import { useTheme } from '../context/ThemeContext'; const ThemeToggle = () => { const { isDark, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme} className="fixed top-6 right-6 p-3 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors z-50 shadow-md" aria-label="Toggle theme" > {isDark ? ( <svg className="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" /> </svg> ) : ( <svg className="w-5 h-5 text-gray-700" fill="currentColor" viewBox="0 0 20 20"> <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> </svg> )} </button> ); }; export default ThemeToggle; COMMAND_BLOCK: import React from 'react'; import { useTheme } from '../context/ThemeContext'; const ThemeToggle = () => { const { isDark, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme} className="fixed top-6 right-6 p-3 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors z-50 shadow-md" aria-label="Toggle theme" > {isDark ? ( <svg className="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" /> </svg> ) : ( <svg className="w-5 h-5 text-gray-700" fill="currentColor" viewBox="0 0 20 20"> <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> </svg> )} </button> ); }; export default ThemeToggle; COMMAND_BLOCK: import React from 'react'; const Home = () => { return ( <div className="min-h-screen flex items-center justify-center bg-white dark:bg-gray-900"> <div className="max-w-3xl mx-auto px-6 text-center"> <h1 className="text-5xl md:text-6xl font-light text-gray-900 dark:text-white mb-6 tracking-tight"> Alex Chen </h1> <p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto"> Software Engineer specializing in Cloud Infrastructure, DevOps, and Embedded Systems </p> <div className="flex flex-wrap justify-center gap-3 mb-12"> {['☁️ AWS', 'πŸš€ Kubernetes', 'πŸ’» React', 'πŸ”§ Embedded C', 'πŸ“¦ Terraform', 'πŸ€– CI/CD'].map((skill) => ( <span key={skill} className="px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full text-sm font-medium" > {skill} </span> ))} </div> <div className="flex justify-center space-x-4"> <a href="/projects" className="px-6 py-3 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors font-medium" > View My Work </a> <a href="/about" className="px-6 py-3 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:border-gray-900 dark:hover:border-white hover:text-gray-900 dark:hover:text-white transition-colors font-medium" > About Me </a> </div> </div> </div> ); }; export default Home; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import React from 'react'; const Home = () => { return ( <div className="min-h-screen flex items-center justify-center bg-white dark:bg-gray-900"> <div className="max-w-3xl mx-auto px-6 text-center"> <h1 className="text-5xl md:text-6xl font-light text-gray-900 dark:text-white mb-6 tracking-tight"> Alex Chen </h1> <p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto"> Software Engineer specializing in Cloud Infrastructure, DevOps, and Embedded Systems </p> <div className="flex flex-wrap justify-center gap-3 mb-12"> {['☁️ AWS', 'πŸš€ Kubernetes', 'πŸ’» React', 'πŸ”§ Embedded C', 'πŸ“¦ Terraform', 'πŸ€– CI/CD'].map((skill) => ( <span key={skill} className="px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full text-sm font-medium" > {skill} </span> ))} </div> <div className="flex justify-center space-x-4"> <a href="/projects" className="px-6 py-3 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors font-medium" > View My Work </a> <a href="/about" className="px-6 py-3 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:border-gray-900 dark:hover:border-white hover:text-gray-900 dark:hover:text-white transition-colors font-medium" > About Me </a> </div> </div> </div> ); }; export default Home; COMMAND_BLOCK: import React from 'react'; const Home = () => { return ( <div className="min-h-screen flex items-center justify-center bg-white dark:bg-gray-900"> <div className="max-w-3xl mx-auto px-6 text-center"> <h1 className="text-5xl md:text-6xl font-light text-gray-900 dark:text-white mb-6 tracking-tight"> Alex Chen </h1> <p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto"> Software Engineer specializing in Cloud Infrastructure, DevOps, and Embedded Systems </p> <div className="flex flex-wrap justify-center gap-3 mb-12"> {['☁️ AWS', 'πŸš€ Kubernetes', 'πŸ’» React', 'πŸ”§ Embedded C', 'πŸ“¦ Terraform', 'πŸ€– CI/CD'].map((skill) => ( <span key={skill} className="px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full text-sm font-medium" > {skill} </span> ))} </div> <div className="flex justify-center space-x-4"> <a href="/projects" className="px-6 py-3 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors font-medium" > View My Work </a> <a href="/about" className="px-6 py-3 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:border-gray-900 dark:hover:border-white hover:text-gray-900 dark:hover:text-white transition-colors font-medium" > About Me </a> </div> </div> </div> ); }; export default Home; COMMAND_BLOCK: import React, { useState, useEffect } from 'react'; import axios from 'axios'; const Projects = () => { const [projects, setProjects] = useState([]); const [filter, setFilter] = useState('all'); const [loading, setLoading] = useState(true); const categories = ['all', 'Cloud', 'DevOps', 'Web Dev', 'Embedded']; useEffect(() => { const fetchProjects = async () => { try { // Replace with your actual API endpoint const response = await axios.get('http://localhost:3001/api/projects'); setProjects(response.data); } catch (error) { console.error('Error fetching projects:', error); // Fallback sample data setProjects([ { id: 1, title: 'Cloud Infrastructure Automation', description: 'Scalable AWS infrastructure using Terraform and Kubernetes', category: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format', technologies: ['Terraform', 'AWS', 'K8s'], }, // Add more projects... ]); } finally { setLoading(false); } }; fetchProjects(); }, []); const filteredProjects = filter === 'all' ? projects : projects.filter(p => p.category === filter); return ( <div className="min-h-screen bg-white dark:bg-gray-900 pt-24 pb-16"> <div className="max-w-6xl mx-auto px-6"> <h2 className="text-3xl font-light text-gray-900 dark:text-white mb-8"> Featured Projects </h2> {/* Filter buttons */} <div className="flex flex-wrap gap-2 mb-10"> {categories.map((cat) => ( <button key={cat} onClick={() => setFilter(cat)} className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${ filter === cat ? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' }`} > {cat === 'all' ? 'All Projects' : cat} </button> ))} </div> {/* Projects grid */} {loading ? ( <div className="text-center py-12"> <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 dark:border-gray-600 border-t-gray-900 dark:border-t-white"></div> </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> {filteredProjects.map((project) => ( <div key={project.id} className="group bg-gray-50 dark:bg-gray-800 rounded-xl overflow-hidden hover:shadow-xl transition-all duration-300" > <div className="overflow-hidden"> <img src={project.image} alt={project.title} className="w-full h-56 object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <div className="p-6"> <div className="flex items-center justify-between mb-3"> <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> {project.category} </span> <div className="flex gap-2"> {project.technologies.slice(0, 3).map((tech) => ( <span key={tech} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded" > {tech} </span> ))} </div> </div> <h3 className="text-xl font-medium text-gray-900 dark:text-white mb-2"> {project.title} </h3> <p className="text-gray-600 dark:text-gray-400 text-sm mb-4"> {project.description} </p> <button className="text-sm font-medium text-gray-900 dark:text-white hover:underline"> View Case Study β†’ </button> </div> </div> ))} </div> )} </div> </div> ); }; export default Projects; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import React, { useState, useEffect } from 'react'; import axios from 'axios'; const Projects = () => { const [projects, setProjects] = useState([]); const [filter, setFilter] = useState('all'); const [loading, setLoading] = useState(true); const categories = ['all', 'Cloud', 'DevOps', 'Web Dev', 'Embedded']; useEffect(() => { const fetchProjects = async () => { try { // Replace with your actual API endpoint const response = await axios.get('http://localhost:3001/api/projects'); setProjects(response.data); } catch (error) { console.error('Error fetching projects:', error); // Fallback sample data setProjects([ { id: 1, title: 'Cloud Infrastructure Automation', description: 'Scalable AWS infrastructure using Terraform and Kubernetes', category: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format', technologies: ['Terraform', 'AWS', 'K8s'], }, // Add more projects... ]); } finally { setLoading(false); } }; fetchProjects(); }, []); const filteredProjects = filter === 'all' ? projects : projects.filter(p => p.category === filter); return ( <div className="min-h-screen bg-white dark:bg-gray-900 pt-24 pb-16"> <div className="max-w-6xl mx-auto px-6"> <h2 className="text-3xl font-light text-gray-900 dark:text-white mb-8"> Featured Projects </h2> {/* Filter buttons */} <div className="flex flex-wrap gap-2 mb-10"> {categories.map((cat) => ( <button key={cat} onClick={() => setFilter(cat)} className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${ filter === cat ? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' }`} > {cat === 'all' ? 'All Projects' : cat} </button> ))} </div> {/* Projects grid */} {loading ? ( <div className="text-center py-12"> <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 dark:border-gray-600 border-t-gray-900 dark:border-t-white"></div> </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> {filteredProjects.map((project) => ( <div key={project.id} className="group bg-gray-50 dark:bg-gray-800 rounded-xl overflow-hidden hover:shadow-xl transition-all duration-300" > <div className="overflow-hidden"> <img src={project.image} alt={project.title} className="w-full h-56 object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <div className="p-6"> <div className="flex items-center justify-between mb-3"> <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> {project.category} </span> <div className="flex gap-2"> {project.technologies.slice(0, 3).map((tech) => ( <span key={tech} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded" > {tech} </span> ))} </div> </div> <h3 className="text-xl font-medium text-gray-900 dark:text-white mb-2"> {project.title} </h3> <p className="text-gray-600 dark:text-gray-400 text-sm mb-4"> {project.description} </p> <button className="text-sm font-medium text-gray-900 dark:text-white hover:underline"> View Case Study β†’ </button> </div> </div> ))} </div> )} </div> </div> ); }; export default Projects; COMMAND_BLOCK: import React, { useState, useEffect } from 'react'; import axios from 'axios'; const Projects = () => { const [projects, setProjects] = useState([]); const [filter, setFilter] = useState('all'); const [loading, setLoading] = useState(true); const categories = ['all', 'Cloud', 'DevOps', 'Web Dev', 'Embedded']; useEffect(() => { const fetchProjects = async () => { try { // Replace with your actual API endpoint const response = await axios.get('http://localhost:3001/api/projects'); setProjects(response.data); } catch (error) { console.error('Error fetching projects:', error); // Fallback sample data setProjects([ { id: 1, title: 'Cloud Infrastructure Automation', description: 'Scalable AWS infrastructure using Terraform and Kubernetes', category: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format', technologies: ['Terraform', 'AWS', 'K8s'], }, // Add more projects... ]); } finally { setLoading(false); } }; fetchProjects(); }, []); const filteredProjects = filter === 'all' ? projects : projects.filter(p => p.category === filter); return ( <div className="min-h-screen bg-white dark:bg-gray-900 pt-24 pb-16"> <div className="max-w-6xl mx-auto px-6"> <h2 className="text-3xl font-light text-gray-900 dark:text-white mb-8"> Featured Projects </h2> {/* Filter buttons */} <div className="flex flex-wrap gap-2 mb-10"> {categories.map((cat) => ( <button key={cat} onClick={() => setFilter(cat)} className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${ filter === cat ? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' }`} > {cat === 'all' ? 'All Projects' : cat} </button> ))} </div> {/* Projects grid */} {loading ? ( <div className="text-center py-12"> <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 dark:border-gray-600 border-t-gray-900 dark:border-t-white"></div> </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> {filteredProjects.map((project) => ( <div key={project.id} className="group bg-gray-50 dark:bg-gray-800 rounded-xl overflow-hidden hover:shadow-xl transition-all duration-300" > <div className="overflow-hidden"> <img src={project.image} alt={project.title} className="w-full h-56 object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <div className="p-6"> <div className="flex items-center justify-between mb-3"> <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> {project.category} </span> <div className="flex gap-2"> {project.technologies.slice(0, 3).map((tech) => ( <span key={tech} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded" > {tech} </span> ))} </div> </div> <h3 className="text-xl font-medium text-gray-900 dark:text-white mb-2"> {project.title} </h3> <p className="text-gray-600 dark:text-gray-400 text-sm mb-4"> {project.description} </p> <button className="text-sm font-medium text-gray-900 dark:text-white hover:underline"> View Case Study β†’ </button> </div> </div> ))} </div> )} </div> </div> ); }; export default Projects; CODE_BLOCK: import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { ThemeProvider } from './context/ThemeContext'; import Navbar from './components/Navbar'; import ThemeToggle from './components/ThemeToggle'; import Home from './pages/Home'; import Projects from './pages/Projects'; import About from './pages/About'; function App() { return ( <ThemeProvider> <Router> <div className="min-h-screen bg-white dark:bg-gray-900"> <Navbar /> <ThemeToggle /> <Routes> <Route path="/" element={<Home />} /> <Route path="/projects" element={<Projects />} /> <Route path="/about" element={<About />} /> </Routes> </div> </Router> </ThemeProvider> ); } export default App; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { ThemeProvider } from './context/ThemeContext'; import Navbar from './components/Navbar'; import ThemeToggle from './components/ThemeToggle'; import Home from './pages/Home'; import Projects from './pages/Projects'; import About from './pages/About'; function App() { return ( <ThemeProvider> <Router> <div className="min-h-screen bg-white dark:bg-gray-900"> <Navbar /> <ThemeToggle /> <Routes> <Route path="/" element={<Home />} /> <Route path="/projects" element={<Projects />} /> <Route path="/about" element={<About />} /> </Routes> </div> </Router> </ThemeProvider> ); } export default App; CODE_BLOCK: import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { ThemeProvider } from './context/ThemeContext'; import Navbar from './components/Navbar'; import ThemeToggle from './components/ThemeToggle'; import Home from './pages/Home'; import Projects from './pages/Projects'; import About from './pages/About'; function App() { return ( <ThemeProvider> <Router> <div className="min-h-screen bg-white dark:bg-gray-900"> <Navbar /> <ThemeToggle /> <Routes> <Route path="/" element={<Home />} /> <Route path="/projects" element={<Projects />} /> <Route path="/about" element={<About />} /> </Routes> </div> </Router> </ThemeProvider> ); } export default App; COMMAND_BLOCK: const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json()); // Projects API endpoint app.get('/api/projects', (req, res) => { const projects = [ { id: 1, title: 'Cloud Infrastructure Automation', description: 'Scalable AWS infrastructure using Terraform and Kubernetes', category: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format', technologies: ['Terraform', 'AWS', 'K8s'], }, // Add more projects... ]; res.json(projects); }); app.listen(3001, () => { console.log('Server running on port 3001'); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json()); // Projects API endpoint app.get('/api/projects', (req, res) => { const projects = [ { id: 1, title: 'Cloud Infrastructure Automation', description: 'Scalable AWS infrastructure using Terraform and Kubernetes', category: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format', technologies: ['Terraform', 'AWS', 'K8s'], }, // Add more projects... ]; res.json(projects); }); app.listen(3001, () => { console.log('Server running on port 3001'); }); COMMAND_BLOCK: const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json()); // Projects API endpoint app.get('/api/projects', (req, res) => { const projects = [ { id: 1, title: 'Cloud Infrastructure Automation', description: 'Scalable AWS infrastructure using Terraform and Kubernetes', category: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format', technologies: ['Terraform', 'AWS', 'K8s'], }, // Add more projects... ]; res.json(projects); }); app.listen(3001, () => { console.log('Server running on port 3001'); }); COMMAND_BLOCK: npm run build Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm run build COMMAND_BLOCK: npm run build COMMAND_BLOCK: npm install -g vercel vercel Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm install -g vercel vercel COMMAND_BLOCK: npm install -g vercel vercel COMMAND_BLOCK: npm run build # Drag and drop the 'dist' folder to Netlify Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm run build # Drag and drop the 'dist' folder to Netlify COMMAND_BLOCK: npm run build # Drag and drop the 'dist' folder to Netlify CODE_BLOCK: <meta name="description" content="Portfolio of Alex Chen - Software Engineer specializing in Cloud, DevOps, and Embedded Systems"> <meta name="keywords" content="software engineer, portfolio, cloud, devops, react, tailwind"> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <meta name="description" content="Portfolio of Alex Chen - Software Engineer specializing in Cloud, DevOps, and Embedded Systems"> <meta name="keywords" content="software engineer, portfolio, cloud, devops, react, tailwind"> CODE_BLOCK: <meta name="description" content="Portfolio of Alex Chen - Software Engineer specializing in Cloud, DevOps, and Embedded Systems"> <meta name="keywords" content="software engineer, portfolio, cloud, devops, react, tailwind"> - Vite: Lightning-fast build tool with instant hot module replacement - React: Component-based architecture for reusable UI elements - Tailwind CSS v4: Utility-first CSS with JIT compilation and dark mode support - React Router: Seamless client-side navigation - Axios: Promise-based HTTP client for API integration - Node.js (v18 or higher) - npm or yarn - Basic knowledge of React and Tailwind - Dark/Light Mode Toggle - Persistent theme switching with system preference detection - Responsive Design - Mobile-first approach using Tailwind's responsive utilities - Project Filtering - Category-based filtering with smooth transitions - Loading States - Spinner indicators for async operations - Backend Ready - Axios integration for API calls - Smooth Animations - Hover effects and transitions - Clean Navigation - React Router with active link highlighting - Add meta tags in index.html: - Use semantic HTML (header, main, section, article) - Add alt text to all images - Implement proper heading hierarchy (h1, h2, h3) - Add a blog section with MDX support - Implement a contact form with EmailJS - Add smooth scroll animations with Framer Motion - Include a resume download button - Add testimonials carousel - Implement portfolio item modals - Tailwind CSS v4 Documentation - React Documentation - Vite Documentation - Unsplash - Free images for your projects - Performance: Use lazy loading for images and code splitting for routes - Accessibility: Ensure proper contrast ratios and keyboard navigation - Testing: Add unit tests for components using Jest and React Testing Library - Analytics: Integrate Google Analytics or Plausible for visitor insights - βœ… Lightning-fast development with Vite - βœ… Beautiful, responsive design with Tailwind CSS v4 - βœ… Dark mode with system preference detection - βœ… Dynamic content with API integration - βœ… Clean, maintainable code structure