🎨 Module Project: Portfolio Landing Page
Congratulations on mastering the React Basics! You've learned about components, props, JSX/TSX, styling, and event handling. Now it's time to bring it all together. In this module project, you'll build a professional personal portfolio landing page that showcases your skills as a developer. You'll create reusable components, handle user interactions, apply beautiful styling, and build something you can actually use in the real world! 🚀
🎯 Project Objectives
By completing this project, you will:
- Build a complete, multi-section website using React components
- Create reusable UI components with typed props
- Implement responsive design that works on all devices
- Apply multiple styling approaches (inline, CSS modules, or Tailwind)
- Handle user interactions with properly typed event handlers
- Compose components to build complex layouts
- Use conditional rendering for interactive features
- Implement a contact form with validation
Estimated Time: 2-3 hours
Difficulty: Intermediate - applies all Module 2 concepts
💡 What You'll Build
A complete portfolio landing page with:
- Hero section with your name and tagline
- About section showcasing your background
- Skills section with skill cards
- Projects section displaying your work
- Contact form with validation
- Responsive navigation bar
- Interactive elements and smooth animations
📑 Project Sections
📋 Project Requirements
Let's start by understanding exactly what we're building. A portfolio landing page is your chance to make a great first impression as a developer. It should be visually appealing, easy to navigate, and showcase your work effectively.
Core Requirements
Page Sections Required
- Navigation Bar - Fixed/sticky header with links to all sections
- Hero Section - Eye-catching introduction with your name and title
- About Section - Brief bio and background information
- Skills Section - Display of your technical skills with visual cards
- Projects Section - Showcase of 3-6 projects with descriptions
- Contact Section - Working contact form with validation
- Footer - Social links and copyright information
Technical Requirements
- Built with React functional components
- All components properly typed with TypeScript
- Responsive design (mobile, tablet, desktop)
- Proper use of props for component reusability
- Event handlers for all interactive elements
- Form validation with error messages
- Smooth scroll navigation between sections
- Accessible (semantic HTML, ARIA labels where needed)
Styling Requirements
- Choose one primary styling approach (inline styles, CSS Modules, or Tailwind)
- Consistent color scheme and typography
- Hover effects and transitions
- Cards with shadows and borders for visual hierarchy
- Responsive breakpoints for different screen sizes
- Professional and modern appearance
Component Structure Overview
graph TD
A[App] --> B[Navigation]
A --> C[Hero]
A --> D[About]
A --> E[Skills]
A --> F[Projects]
A --> G[Contact]
A --> H[Footer]
E --> E1[SkillCard]
E --> E2[SkillCard]
E --> E3[SkillCard]
F --> F1[ProjectCard]
F --> F2[ProjectCard]
F --> F3[ProjectCard]
G --> G1[ContactForm]
style A fill:#667eea,color:#fff
style B fill:#764ba2,color:#fff
style C fill:#764ba2,color:#fff
style D fill:#764ba2,color:#fff
style E fill:#764ba2,color:#fff
style F fill:#764ba2,color:#fff
style G fill:#764ba2,color:#fff
style H fill:#764ba2,color:#fff
TypeScript Concepts to Apply
| Concept | Where to Use | Example |
|---|---|---|
| Interfaces | Define prop types | interface ProjectCardProps |
| Type Aliases | Reusable types | type Skill = { name: string; level: number } |
| React.FC | Component typing | const Hero: React.FC = () => {...} |
| Event Types | Form handlers | React.FormEvent, React.ChangeEvent |
| Union Types | Skill levels | 'beginner' | 'intermediate' | 'advanced' |
| Optional Props | Component flexibility | description?: string |
💡 Success Criteria
Your portfolio is complete when:
- ✅ All sections are present and functional
- ✅ Navigation links scroll smoothly to each section
- ✅ All components have proper TypeScript types
- ✅ Form validates input and shows appropriate errors
- ✅ Page is responsive on mobile, tablet, and desktop
- ✅ No TypeScript errors in the console
- ✅ Professional appearance with consistent styling
- ✅ Code is well-organized and readable
⚙️ Project Setup
Before we start building, let's set up our project properly. We'll use Vite for fast development and configure TypeScript correctly.
Step 1: Create the Project
Using Vite
# Create new Vite project with React and TypeScript
npm create vite@latest portfolio -- --template react-ts
# Navigate into the project
cd portfolio
# Install dependencies
npm install
# Start development server
npm run dev
Your project should now be running at http://localhost:5173
Step 2: Project Structure
Recommended Folder Structure
portfolio/
├── src/
│ ├── components/
│ │ ├── Navigation.tsx
│ │ ├── Hero.tsx
│ │ ├── About.tsx
│ │ ├── Skills.tsx
│ │ ├── SkillCard.tsx
│ │ ├── Projects.tsx
│ │ ├── ProjectCard.tsx
│ │ ├── Contact.tsx
│ │ ├── ContactForm.tsx
│ │ └── Footer.tsx
│ ├── types/
│ │ └── index.ts
│ ├── data/
│ │ └── portfolio.ts
│ ├── App.tsx
│ ├── App.css
│ └── main.tsx
├── public/
├── index.html
├── package.json
└── tsconfig.json
Step 3: Create Type Definitions
src/types/index.ts
Let's define all our TypeScript types in one place:
// Skill type with name, level, and icon
export interface Skill {
name: string;
level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
icon?: string; // Optional emoji or icon
}
// Project type for portfolio items
export interface Project {
id: number;
title: string;
description: string;
technologies: string[];
imageUrl?: string;
demoUrl?: string;
githubUrl?: string;
}
// Contact form data type
export interface ContactFormData {
name: string;
email: string;
message: string;
}
// Form validation errors
export interface FormErrors {
name?: string;
email?: string;
message?: string;
}
// Navigation link type
export interface NavLink {
label: string;
href: string;
}
// Social link type for footer
export interface SocialLink {
platform: string;
url: string;
icon: string;
}
Step 4: Create Sample Data
src/data/portfolio.ts
Create sample data to populate your portfolio:
import { Skill, Project, NavLink, SocialLink } from '../types';
export const skills: Skill[] = [
{ name: 'TypeScript', level: 'advanced', icon: '📘' },
{ name: 'React', level: 'advanced', icon: '⚛️' },
{ name: 'JavaScript', level: 'expert', icon: '💛' },
{ name: 'HTML/CSS', level: 'expert', icon: '🎨' },
{ name: 'Node.js', level: 'intermediate', icon: '🟢' },
{ name: 'Git', level: 'advanced', icon: '🔧' },
];
export const projects: Project[] = [
{
id: 1,
title: 'E-Commerce Platform',
description: 'A full-stack e-commerce application with cart, checkout, and payment integration.',
technologies: ['React', 'TypeScript', 'Node.js', 'MongoDB'],
demoUrl: 'https://example.com',
githubUrl: 'https://github.com/username/project',
},
{
id: 2,
title: 'Task Management App',
description: 'Collaborative task manager with real-time updates and team features.',
technologies: ['React', 'TypeScript', 'Firebase', 'Tailwind CSS'],
demoUrl: 'https://example.com',
githubUrl: 'https://github.com/username/project',
},
{
id: 3,
title: 'Weather Dashboard',
description: 'Real-time weather dashboard with forecasts and location search.',
technologies: ['React', 'TypeScript', 'Weather API', 'Chart.js'],
demoUrl: 'https://example.com',
githubUrl: 'https://github.com/username/project',
},
];
export const navLinks: NavLink[] = [
{ label: 'Home', href: '#home' },
{ label: 'About', href: '#about' },
{ label: 'Skills', href: '#skills' },
{ label: 'Projects', href: '#projects' },
{ label: 'Contact', href: '#contact' },
];
export const socialLinks: SocialLink[] = [
{ platform: 'GitHub', url: 'https://github.com/username', icon: '🐙' },
{ platform: 'LinkedIn', url: 'https://linkedin.com/in/username', icon: '💼' },
{ platform: 'Twitter', url: 'https://twitter.com/username', icon: '🐦' },
];
Note: Replace these with your own information as you build!
✅ Setup Complete!
You now have:
- A working React + TypeScript project with Vite
- Organized folder structure
- Type definitions for all data structures
- Sample data to work with
Let's start building components!
🏗️ Component Architecture
Before we dive into coding individual components, let's understand how they'll work together. Good component architecture makes your code maintainable and reusable.
Component Hierarchy
Top-Level Structure
The App.tsx component will be our main container that renders all sections:
import Navigation from './components/Navigation';
import Hero from './components/Hero';
import About from './components/About';
import Skills from './components/Skills';
import Projects from './components/Projects';
import Contact from './components/Contact';
import Footer from './components/Footer';
function App() {
return (
<>
<Navigation />
<main>
<Hero />
<About />
<Skills />
<Projects />
<Contact />
</main>
<Footer />
</>
);
}
export default App;
Component Categories
| Category | Components | Purpose |
|---|---|---|
| Layout | Navigation, Footer | Structure and navigation |
| Sections | Hero, About, Skills, Projects, Contact | Main content areas |
| Reusable Cards | SkillCard, ProjectCard | Display repeated data |
| Forms | ContactForm | User input and validation |
Data Flow Pattern
graph LR
A[portfolio.ts
Sample Data] --> B[App.tsx]
B --> C[Skills Component]
B --> D[Projects Component]
C --> E[SkillCard
Props: skill]
D --> F[ProjectCard
Props: project]
style A fill:#f9f9f9
style B fill:#667eea,color:#fff
style C fill:#764ba2,color:#fff
style D fill:#764ba2,color:#fff
style E fill:#e3f2fd
style F fill:#e3f2fd
💡 Component Design Principles
- Single Responsibility: Each component does one thing well
- Reusability: Cards accept props to display different data
- Composition: Build complex UIs from simple components
- Type Safety: All props are properly typed with TypeScript
- Separation of Concerns: Data, types, and components in separate files
Styling Strategy
Choose Your Approach
For this project, you can use any styling method you prefer. Here are recommendations:
| Method | Pros | Best For |
|---|---|---|
| Inline Styles | Quick, component-scoped | Small projects, prototypes |
| CSS Modules | Scoped styles, familiar CSS | Medium projects |
| Tailwind CSS | Utility-first, fast development | Modern, professional sites |
For this tutorial, we'll use inline styles for simplicity, but feel free to convert to your preferred method!
📦 Component Checklist
Each component we build will have:
- ✅ Proper TypeScript typing (props interface if needed)
- ✅ React.FC type annotation
- ✅ Clear, descriptive naming
- ✅ Responsive styling
- ✅ Accessibility considerations
- ✅ Comments explaining complex logic
🧭 Building the Navigation
Let's start by building the navigation bar - the component that helps users navigate through your portfolio. It will be sticky, responsive, and include smooth scrolling.
Navigation Component Structure
What We're Building
- Fixed/sticky navigation bar at the top
- Logo/name on the left
- Navigation links on the right
- Smooth scroll to sections when clicked
- Mobile-friendly hamburger menu (optional enhancement)
Step 1: Create Navigation Component
src/components/Navigation.tsx
import React from 'react';
import { navLinks } from '../data/portfolio';
const Navigation: React.FC = () => {
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
e.preventDefault();
const targetId = href.replace('#', '');
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
};
return (
<nav style={styles.nav}>
<div style={styles.container}>
<h1 style={styles.logo}>Your Name</h1>
<ul style={styles.navList}>
{navLinks.map((link) => (
<li key={link.href} style={styles.navItem}>
<a
href={link.href}
onClick={(e) => handleSmoothScroll(e, link.href)}
style={styles.navLink}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</nav>
);
};
const styles = {
nav: {
position: 'fixed' as const,
top: 0,
left: 0,
right: 0,
backgroundColor: '#ffffff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
zIndex: 1000,
padding: '1rem 0',
},
container: {
maxWidth: '1200px',
margin: '0 auto',
padding: '0 2rem',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
logo: {
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#667eea',
margin: 0,
},
navList: {
display: 'flex',
listStyle: 'none',
gap: '2rem',
margin: 0,
padding: 0,
},
navItem: {
margin: 0,
},
navLink: {
textDecoration: 'none',
color: '#333',
fontSize: '1rem',
fontWeight: 500,
transition: 'color 0.3s ease',
cursor: 'pointer',
},
};
export default Navigation;
💡 Key Features Explained
- Fixed positioning: Navigation stays at top while scrolling
- Smooth scrolling:
scrollIntoViewcreates smooth transitions - Event prevention:
preventDefault()stops default anchor behavior - Type safety: Event handler properly typed with
React.MouseEvent - Dynamic links: Maps over
navLinksarray for maintainability
Adding Hover Effects
Enhanced Navigation with Hover States
To add hover effects with inline styles, we can use the onMouseEnter and onMouseLeave events:
import React, { useState } from 'react';
import { navLinks } from '../data/portfolio';
const Navigation: React.FC = () => {
const [hoveredLink, setHoveredLink] = useState<string | null>(null);
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
e.preventDefault();
const targetId = href.replace('#', '');
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
};
return (
<nav style={styles.nav}>
<div style={styles.container}>
<h1 style={styles.logo}>Your Name</h1>
<ul style={styles.navList}>
{navLinks.map((link) => (
<li key={link.href} style={styles.navItem}>
<a
href={link.href}
onClick={(e) => handleSmoothScroll(e, link.href)}
onMouseEnter={() => setHoveredLink(link.href)}
onMouseLeave={() => setHoveredLink(null)}
style={{
...styles.navLink,
color: hoveredLink === link.href ? '#667eea' : '#333',
}}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</nav>
);
};
export default Navigation;
✅ Navigation Complete!
You now have a functional, sticky navigation with:
- ✅ Fixed positioning at the top
- ✅ Smooth scrolling to sections
- ✅ Hover effects for better UX
- ✅ Properly typed event handlers
🦸 Creating the Hero Section
The Hero section is the first thing visitors see - it should immediately grab attention and communicate who you are. Let's make it impressive!
Hero Component Design
What We're Building
- Full-screen or large hero section
- Your name in large, bold text
- Tagline describing what you do
- Call-to-action buttons (View Projects, Contact Me)
- Gradient or eye-catching background
Step 1: Create Hero Component
src/components/Hero.tsx
import React from 'react';
const Hero: React.FC = () => {
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<section id="home" style={styles.hero}>
<div style={styles.content}>
<h1 style={styles.name}>
Hi, I'm <span style={styles.highlight}>Your Name</span>
</h1>
<p style={styles.tagline}>
Full-Stack Developer | React Enthusiast | TypeScript Advocate
</p>
<p style={styles.description}>
I build beautiful, functional web applications that solve real problems.
Passionate about clean code, user experience, and continuous learning.
</p>
<div style={styles.buttonGroup}>
<button
onClick={() => scrollToSection('projects')}
style={styles.primaryButton}
>
View My Work
</button>
<button
onClick={() => scrollToSection('contact')}
style={styles.secondaryButton}
>
Contact Me
</button>
</div>
</div>
</section>
);
};
const styles = {
hero: {
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#ffffff',
textAlign: 'center' as const,
padding: '2rem',
marginTop: '60px', // Account for fixed nav
},
content: {
maxWidth: '800px',
},
name: {
fontSize: '3rem',
fontWeight: 'bold',
marginBottom: '1rem',
lineHeight: 1.2,
},
highlight: {
color: '#ffd700',
},
tagline: {
fontSize: '1.5rem',
marginBottom: '1.5rem',
fontWeight: 500,
},
description: {
fontSize: '1.125rem',
marginBottom: '2rem',
lineHeight: 1.6,
opacity: 0.95,
},
buttonGroup: {
display: 'flex',
gap: '1rem',
justifyContent: 'center',
flexWrap: 'wrap' as const,
},
primaryButton: {
padding: '0.875rem 2rem',
fontSize: '1rem',
fontWeight: 600,
border: 'none',
borderRadius: '8px',
backgroundColor: '#ffffff',
color: '#667eea',
cursor: 'pointer',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
},
secondaryButton: {
padding: '0.875rem 2rem',
fontSize: '1rem',
fontWeight: 600,
border: '2px solid #ffffff',
borderRadius: '8px',
backgroundColor: 'transparent',
color: '#ffffff',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
},
};
export default Hero;
Adding Interactive Button Effects
Enhanced Hero with Hover States
import React, { useState } from 'react';
const Hero: React.FC = () => {
const [hoveredButton, setHoveredButton] = useState<string | null>(null);
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
const getPrimaryButtonStyle = () => ({
...styles.primaryButton,
transform: hoveredButton === 'primary' ? 'translateY(-2px)' : 'translateY(0)',
boxShadow: hoveredButton === 'primary'
? '0 6px 12px rgba(0, 0, 0, 0.2)'
: '0 4px 6px rgba(0, 0, 0, 0.1)',
});
const getSecondaryButtonStyle = () => ({
...styles.secondaryButton,
backgroundColor: hoveredButton === 'secondary' ? 'rgba(255, 255, 255, 0.1)' : 'transparent',
});
return (
<section id="home" style={styles.hero}>
<div style={styles.content}>
<h1 style={styles.name}>
Hi, I'm <span style={styles.highlight}>Your Name</span>
</h1>
<p style={styles.tagline}>
Full-Stack Developer | React Enthusiast | TypeScript Advocate
</p>
<p style={styles.description}>
I build beautiful, functional web applications that solve real problems.
Passionate about clean code, user experience, and continuous learning.
</p>
<div style={styles.buttonGroup}>
<button
onClick={() => scrollToSection('projects')}
onMouseEnter={() => setHoveredButton('primary')}
onMouseLeave={() => setHoveredButton(null)}
style={getPrimaryButtonStyle()}
>
View My Work
</button>
<button
onClick={() => scrollToSection('contact')}
onMouseEnter={() => setHoveredButton('secondary')}
onMouseLeave={() => setHoveredButton(null)}
style={getSecondaryButtonStyle()}
>
Contact Me
</button>
</div>
</div>
</section>
);
};
export default Hero;
💡 Design Decisions Explained
- Full-height section:
minHeight: '100vh'creates impactful first impression - Gradient background: Modern, eye-catching visual
- Highlight color: Golden accent draws attention to your name
- Button hierarchy: Primary (filled) vs Secondary (outlined)
- Responsive text: Large text sizes scale with viewport
✅ Hero Section Complete!
Your hero section now has:
- ✅ Eye-catching gradient background
- ✅ Clear name and tagline
- ✅ Call-to-action buttons with hover effects
- ✅ Smooth scrolling to other sections
👤 About Section
The About section tells your story - who you are, your background, and what drives you as a developer. Let's make it personal and engaging!
About Component Design
What We're Building
- Section heading
- Professional photo or avatar (optional)
- Brief biography (2-3 paragraphs)
- Key highlights or achievements
- Clean, readable layout
Step 1: Create About Component
src/components/About.tsx
import React from 'react';
const About: React.FC = () => {
return (
<section id="about" style={styles.section}>
<div style={styles.container}>
<h2 style={styles.heading}>About Me</h2>
<div style={styles.content}>
<div style={styles.imageContainer}>
{/* Placeholder for your photo */}
<div style={styles.imagePlaceholder}>
<span style={styles.emoji}>👨💻</span>
</div>
</div>
<div style={styles.text}>
<p style={styles.paragraph}>
Hi! I'm a passionate full-stack developer with a love for creating
elegant solutions to complex problems. I specialize in building
modern web applications using React, TypeScript, and Node.js.
</p>
<p style={styles.paragraph}>
My journey into web development started [X years ago], and I've
been hooked ever since. I believe in writing clean, maintainable
code and following best practices to deliver high-quality products.
</p>
<p style={styles.paragraph}>
When I'm not coding, you can find me [your hobbies/interests].
I'm always eager to learn new technologies and take on challenging
projects that push me to grow as a developer.
</p>
<div style={styles.highlights}>
<h3 style={styles.subheading}>Quick Facts</h3>
<ul style={styles.list}>
<li style={styles.listItem}>🎓 Computer Science graduate</li>
<li style={styles.listItem}>💼 [X] years of development experience</li>
<li style={styles.listItem}>🚀 [X]+ projects completed</li>
<li style={styles.listItem}>🌱 Currently learning [new technology]</li>
</ul>
</div>
</div>
</div>
</div>
</section>
);
};
const styles = {
section: {
padding: '5rem 2rem',
backgroundColor: '#f8f9fa',
},
container: {
maxWidth: '1200px',
margin: '0 auto',
},
heading: {
fontSize: '2.5rem',
fontWeight: 'bold',
textAlign: 'center' as const,
marginBottom: '3rem',
color: '#333',
},
content: {
display: 'flex',
gap: '3rem',
alignItems: 'center',
flexWrap: 'wrap' as const,
},
imageContainer: {
flex: '0 0 250px',
},
imagePlaceholder: {
width: '250px',
height: '250px',
borderRadius: '50%',
backgroundColor: '#667eea',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)',
},
emoji: {
fontSize: '6rem',
},
text: {
flex: '1',
minWidth: '300px',
},
paragraph: {
fontSize: '1.125rem',
lineHeight: 1.8,
color: '#555',
marginBottom: '1.5rem',
},
highlights: {
marginTop: '2rem',
padding: '1.5rem',
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)',
},
subheading: {
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '1rem',
color: '#667eea',
},
list: {
listStyle: 'none',
padding: 0,
margin: 0,
},
listItem: {
fontSize: '1.125rem',
marginBottom: '0.75rem',
color: '#555',
},
};
export default About;
Adding a Real Image
Using an Actual Photo
To use a real photo instead of the placeholder:
// Replace the imagePlaceholder div with:
<img
src="/path-to-your-photo.jpg"
alt="Your Name"
style={styles.image}
/>
// Add to styles:
image: {
width: '250px',
height: '250px',
borderRadius: '50%',
objectFit: 'cover' as const,
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)',
},
✅ About Section Complete!
Your about section now includes:
- ✅ Professional layout with image and text
- ✅ Personal biography that tells your story
- ✅ Quick facts highlighting key information
- ✅ Responsive flex layout
💪 Skills Section
The Skills section showcases your technical abilities in a visual, engaging way. We'll create reusable SkillCard components to display each skill with its level.
Component Structure
graph LR
A[Skills Section] --> B[SkillCard]
A --> C[SkillCard]
A --> D[SkillCard]
A --> E[SkillCard]
B --> B1[Skill Name]
B --> B2[Skill Level]
B --> B3[Icon]
style A fill:#667eea,color:#fff
style B fill:#e3f2fd
style C fill:#e3f2fd
style D fill:#e3f2fd
style E fill:#e3f2fd
Step 1: Create SkillCard Component
src/components/SkillCard.tsx
First, let's create a reusable card for individual skills:
import React from 'react';
import { Skill } from '../types';
interface SkillCardProps {
skill: Skill;
}
const SkillCard: React.FC<SkillCardProps> = ({ skill }) => {
// Convert skill level to percentage for visual indicator
const getLevelPercentage = (level: Skill['level']): number => {
switch (level) {
case 'beginner': return 25;
case 'intermediate': return 50;
case 'advanced': return 75;
case 'expert': return 100;
default: return 0;
}
};
// Get color based on skill level
const getLevelColor = (level: Skill['level']): string => {
switch (level) {
case 'beginner': return '#ffc107';
case 'intermediate': return '#2196F3';
case 'advanced': return '#4CAF50';
case 'expert': return '#667eea';
default: return '#ccc';
}
};
const percentage = getLevelPercentage(skill.level);
const color = getLevelColor(skill.level);
return (
<div style={styles.card}>
{skill.icon && (
<div style={styles.icon}>{skill.icon}</div>
)}
<h3 style={styles.skillName}>{skill.name}</h3>
<div style={styles.levelContainer}>
<div style={styles.levelBackground}>
<div
style={{
...styles.levelBar,
width: `${percentage}%`,
backgroundColor: color,
}}
/>
</div>
<span style={styles.levelText}>
{skill.level.charAt(0).toUpperCase() + skill.level.slice(1)}
</span>
</div>
</div>
);
};
const styles = {
card: {
backgroundColor: '#ffffff',
borderRadius: '12px',
padding: '1.5rem',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
cursor: 'pointer',
},
icon: {
fontSize: '3rem',
marginBottom: '1rem',
textAlign: 'center' as const,
},
skillName: {
fontSize: '1.25rem',
fontWeight: 'bold',
marginBottom: '1rem',
textAlign: 'center' as const,
color: '#333',
},
levelContainer: {
marginTop: '1rem',
},
levelBackground: {
width: '100%',
height: '8px',
backgroundColor: '#e0e0e0',
borderRadius: '4px',
overflow: 'hidden',
marginBottom: '0.5rem',
},
levelBar: {
height: '100%',
transition: 'width 0.5s ease',
borderRadius: '4px',
},
levelText: {
fontSize: '0.875rem',
color: '#666',
textAlign: 'center' as const,
display: 'block',
},
};
export default SkillCard;
Step 2: Create Skills Section Component
src/components/Skills.tsx
Now let's create the section that displays all skill cards:
import React, { useState } from 'react';
import { skills } from '../data/portfolio';
import SkillCard from './SkillCard';
const Skills: React.FC = () => {
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
return (
<section id="skills" style={styles.section}>
<div style={styles.container}>
<h2 style={styles.heading}>My Skills</h2>
<p style={styles.intro}>
Here are the technologies and tools I work with. I'm always
learning and expanding my skill set!
</p>
<div style={styles.grid}>
{skills.map((skill, index) => (
<div
key={skill.name}
onMouseEnter={() => setHoveredCard(index)}
onMouseLeave={() => setHoveredCard(null)}
style={{
transform: hoveredCard === index
? 'translateY(-8px)'
: 'translateY(0)',
transition: 'transform 0.3s ease',
}}
>
<SkillCard skill={skill} />
</div>
))}
</div>
</div>
</section>
);
};
const styles = {
section: {
padding: '5rem 2rem',
backgroundColor: '#ffffff',
},
container: {
maxWidth: '1200px',
margin: '0 auto',
},
heading: {
fontSize: '2.5rem',
fontWeight: 'bold',
textAlign: 'center' as const,
marginBottom: '1rem',
color: '#333',
},
intro: {
fontSize: '1.125rem',
textAlign: 'center' as const,
color: '#666',
marginBottom: '3rem',
maxWidth: '600px',
margin: '0 auto 3rem',
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '2rem',
},
};
export default Skills;
💡 Component Design Patterns
- Reusable SkillCard: Accepts
skillprop for flexibility - Type safety: Props interface ensures correct data structure
- Dynamic styling: Level determines color and percentage
- Responsive grid:
auto-fitandminmaxcreate flexible layout - Hover effects: Cards lift on hover for interactivity
- Visual indicators: Progress bars show skill levels at a glance
✅ Skills Section Complete!
Your skills section now has:
- ✅ Reusable SkillCard component with typed props
- ✅ Visual skill level indicators with colors
- ✅ Responsive grid layout
- ✅ Smooth hover animations
- ✅ Dynamic rendering from data array
🚀 Projects Section
The Projects section is where you showcase your best work. We'll create a ProjectCard component to display each project with its details, technologies used, and links to demos and code.
Component Structure
graph LR
A[Projects Section] --> B[ProjectCard]
A --> C[ProjectCard]
A --> D[ProjectCard]
B --> B1[Title]
B --> B2[Description]
B --> B3[Tech Tags]
B --> B4[Links]
style A fill:#667eea,color:#fff
style B fill:#e3f2fd
style C fill:#e3f2fd
style D fill:#e3f2fd
Step 1: Create ProjectCard Component
src/components/ProjectCard.tsx
import React, { useState } from 'react';
import { Project } from '../types';
interface ProjectCardProps {
project: Project;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
const [isHovered, setIsHovered] = useState(false);
return (
<div
style={{
...styles.card,
transform: isHovered ? 'translateY(-8px)' : 'translateY(0)',
boxShadow: isHovered
? '0 12px 24px rgba(0, 0, 0, 0.15)'
: '0 4px 6px rgba(0, 0, 0, 0.1)',
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Project Image Placeholder */}
{project.imageUrl ? (
<img
src={project.imageUrl}
alt={project.title}
style={styles.image}
/>
) : (
<div style={styles.imagePlaceholder}>
<span style={styles.placeholderIcon}>💻</span>
</div>
)}
{/* Project Content */}
<div style={styles.content}>
<h3 style={styles.title}>{project.title}</h3>
<p style={styles.description}>{project.description}</p>
{/* Technology Tags */}
<div style={styles.techTags}>
{project.technologies.map((tech) => (
<span key={tech} style={styles.tag}>
{tech}
</span>
))}
</div>
{/* Project Links */}
<div style={styles.links}>
{project.demoUrl && (
<a
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
style={styles.linkButton}
>
🌐 Live Demo
</a>
)}
{project.githubUrl && (
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
style={styles.linkButton}
>
🐙 View Code
</a>
)}
</div>
</div>
</div>
);
};
const styles = {
card: {
backgroundColor: '#ffffff',
borderRadius: '12px',
overflow: 'hidden',
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
cursor: 'pointer',
},
image: {
width: '100%',
height: '200px',
objectFit: 'cover' as const,
},
imagePlaceholder: {
width: '100%',
height: '200px',
backgroundColor: '#667eea',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
placeholderIcon: {
fontSize: '4rem',
},
content: {
padding: '1.5rem',
},
title: {
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '0.75rem',
color: '#333',
},
description: {
fontSize: '1rem',
lineHeight: 1.6,
color: '#666',
marginBottom: '1rem',
},
techTags: {
display: 'flex',
flexWrap: 'wrap' as const,
gap: '0.5rem',
marginBottom: '1.5rem',
},
tag: {
padding: '0.25rem 0.75rem',
backgroundColor: '#e3f2fd',
color: '#2196F3',
borderRadius: '16px',
fontSize: '0.875rem',
fontWeight: 500,
},
links: {
display: 'flex',
gap: '1rem',
flexWrap: 'wrap' as const,
},
linkButton: {
padding: '0.5rem 1rem',
backgroundColor: '#667eea',
color: '#ffffff',
textDecoration: 'none',
borderRadius: '6px',
fontSize: '0.875rem',
fontWeight: 600,
transition: 'background-color 0.2s ease',
},
};
export default ProjectCard;
Step 2: Create Projects Section Component
src/components/Projects.tsx
import React from 'react';
import { projects } from '../data/portfolio';
import ProjectCard from './ProjectCard';
const Projects: React.FC = () => {
return (
<section id="projects" style={styles.section}>
<div style={styles.container}>
<h2 style={styles.heading}>My Projects</h2>
<p style={styles.intro}>
Here are some of the projects I've worked on. Each one taught me
something new and helped me grow as a developer.
</p>
<div style={styles.grid}>
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
<div style={styles.ctaContainer}>
<p style={styles.ctaText}>
Want to see more? Check out my GitHub for additional projects!
</p>
<a
href="https://github.com/username"
target="_blank"
rel="noopener noreferrer"
style={styles.ctaButton}
>
View All Projects on GitHub
</a>
</div>
</div>
</section>
);
};
const styles = {
section: {
padding: '5rem 2rem',
backgroundColor: '#f8f9fa',
},
container: {
maxWidth: '1200px',
margin: '0 auto',
},
heading: {
fontSize: '2.5rem',
fontWeight: 'bold',
textAlign: 'center' as const,
marginBottom: '1rem',
color: '#333',
},
intro: {
fontSize: '1.125rem',
textAlign: 'center' as const,
color: '#666',
marginBottom: '3rem',
maxWidth: '600px',
margin: '0 auto 3rem',
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
gap: '2rem',
marginBottom: '3rem',
},
ctaContainer: {
textAlign: 'center' as const,
marginTop: '3rem',
},
ctaText: {
fontSize: '1.125rem',
color: '#666',
marginBottom: '1rem',
},
ctaButton: {
display: 'inline-block',
padding: '0.875rem 2rem',
backgroundColor: '#333',
color: '#ffffff',
textDecoration: 'none',
borderRadius: '8px',
fontSize: '1rem',
fontWeight: 600,
transition: 'background-color 0.2s ease',
},
};
export default Projects;
💡 Key Features
- Reusable ProjectCard: Clean separation of concerns
- Conditional rendering: Shows image or placeholder
- Technology tags: Visual display of tech stack
- External links: Opens in new tab with security attributes
- Hover effects: Cards animate on hover for better UX
- CTA section: Encourages visitors to see more work
✅ Projects Section Complete!
Your projects section now features:
- ✅ Reusable ProjectCard component
- ✅ Professional card design with images
- ✅ Technology tag display
- ✅ Demo and GitHub links
- ✅ Responsive grid layout
- ✅ Call-to-action for more projects
📧 Contact Form
The Contact section allows visitors to reach out to you. We'll build a form with proper validation, error handling, and a great user experience.
Form Requirements
What We Need
- Name field (required, min 2 characters)
- Email field (required, valid email format)
- Message field (required, min 10 characters)
- Real-time validation with error messages
- Submit button with disabled state
- Success message after submission
Step 1: Create ContactForm Component
src/components/ContactForm.tsx
import React, { useState } from 'react';
import { ContactFormData, FormErrors } from '../types';
const ContactForm: React.FC = () => {
const [formData, setFormData] = useState<ContactFormData>({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// Validation functions
const validateName = (name: string): string | undefined => {
if (!name.trim()) {
return 'Name is required';
}
if (name.trim().length < 2) {
return 'Name must be at least 2 characters';
}
return undefined;
};
const validateEmail = (email: string): string | undefined => {
if (!email.trim()) {
return 'Email is required';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return 'Please enter a valid email address';
}
return undefined;
};
const validateMessage = (message: string): string | undefined => {
if (!message.trim()) {
return 'Message is required';
}
if (message.trim().length < 10) {
return 'Message must be at least 10 characters';
}
return undefined;
};
// Handle input changes
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error for this field
setErrors(prev => ({ ...prev, [name]: undefined }));
};
// Handle blur events for validation
const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
let error: string | undefined;
switch (name) {
case 'name':
error = validateName(value);
break;
case 'email':
error = validateEmail(value);
break;
case 'message':
error = validateMessage(value);
break;
}
setErrors(prev => ({ ...prev, [name]: error }));
};
// Validate entire form
const validateForm = (): boolean => {
const nameError = validateName(formData.name);
const emailError = validateEmail(formData.email);
const messageError = validateMessage(formData.message);
setErrors({
name: nameError,
email: emailError,
message: messageError,
});
return !nameError && !emailError && !messageError;
};
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('Form submitted:', formData);
setIsSubmitted(true);
setIsSubmitting(false);
// Reset form
setFormData({ name: '', email: '', message: '' });
};
if (isSubmitted) {
return (
<div style={styles.successMessage}>
<h3 style={styles.successTitle}>✅ Message Sent!</h3>
<p style={styles.successText}>
Thank you for reaching out! I'll get back to you as soon as possible.
</p>
<button
onClick={() => setIsSubmitted(false)}
style={styles.resetButton}
>
Send Another Message
</button>
</div>
);
}
return (
<form onSubmit={handleSubmit} style={styles.form}>
{/* Name Field */}
<div style={styles.field}>
<label htmlFor="name" style={styles.label}>
Name *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
style={{
...styles.input,
borderColor: errors.name ? '#f44336' : '#ddd',
}}
placeholder="Your name"
/>
{errors.name && (
<span style={styles.error}>{errors.name}</span>
)}
</div>
{/* Email Field */}
<div style={styles.field}>
<label htmlFor="email" style={styles.label}>
Email *
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
style={{
...styles.input,
borderColor: errors.email ? '#f44336' : '#ddd',
}}
placeholder="your.email@example.com"
/>
{errors.email && (
<span style={styles.error}>{errors.email}</span>
)}
</div>
{/* Message Field */}
<div style={styles.field}>
<label htmlFor="message" style={styles.label}>
Message *
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}
rows={6}
style={{
...styles.textarea,
borderColor: errors.message ? '#f44336' : '#ddd',
}}
placeholder="Your message..."
/>
{errors.message && (
<span style={styles.error}>{errors.message}</span>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
style={{
...styles.submitButton,
opacity: isSubmitting ? 0.6 : 1,
cursor: isSubmitting ? 'not-allowed' : 'pointer',
}}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
};
const styles = {
form: {
maxWidth: '600px',
margin: '0 auto',
},
field: {
marginBottom: '1.5rem',
},
label: {
display: 'block',
marginBottom: '0.5rem',
fontSize: '1rem',
fontWeight: 600,
color: '#333',
},
input: {
width: '100%',
padding: '0.75rem',
fontSize: '1rem',
border: '2px solid #ddd',
borderRadius: '6px',
transition: 'border-color 0.2s ease',
boxSizing: 'border-box' as const,
},
textarea: {
width: '100%',
padding: '0.75rem',
fontSize: '1rem',
border: '2px solid #ddd',
borderRadius: '6px',
transition: 'border-color 0.2s ease',
resize: 'vertical' as const,
fontFamily: 'inherit',
boxSizing: 'border-box' as const,
},
error: {
display: 'block',
marginTop: '0.5rem',
fontSize: '0.875rem',
color: '#f44336',
},
submitButton: {
width: '100%',
padding: '1rem',
fontSize: '1.125rem',
fontWeight: 600,
color: '#ffffff',
backgroundColor: '#667eea',
border: 'none',
borderRadius: '8px',
transition: 'background-color 0.2s ease',
},
successMessage: {
maxWidth: '600px',
margin: '0 auto',
padding: '3rem',
textAlign: 'center' as const,
backgroundColor: '#e8f5e9',
borderRadius: '12px',
},
successTitle: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#4CAF50',
marginBottom: '1rem',
},
successText: {
fontSize: '1.125rem',
color: '#555',
marginBottom: '2rem',
},
resetButton: {
padding: '0.75rem 2rem',
fontSize: '1rem',
fontWeight: 600,
color: '#667eea',
backgroundColor: '#ffffff',
border: '2px solid #667eea',
borderRadius: '8px',
cursor: 'pointer',
},
};
export default ContactForm;
Step 2: Create Contact Section Component
src/components/Contact.tsx
import React from 'react';
import ContactForm from './ContactForm';
const Contact: React.FC = () => {
return (
<section id="contact" style={styles.section}>
<div style={styles.container}>
<h2 style={styles.heading}>Get In Touch</h2>
<p style={styles.intro}>
Have a project in mind or just want to chat? I'd love to hear from you!
Fill out the form below and I'll get back to you as soon as possible.
</p>
<ContactForm />
<div style={styles.alternativeContact}>
<p style={styles.alternativeText}>
Prefer email? Reach me directly at{' '}
<a href="mailto:your.email@example.com" style={styles.emailLink}>
your.email@example.com
</a>
</p>
</div>
</div>
</section>
);
};
const styles = {
section: {
padding: '5rem 2rem',
backgroundColor: '#ffffff',
},
container: {
maxWidth: '1200px',
margin: '0 auto',
},
heading: {
fontSize: '2.5rem',
fontWeight: 'bold',
textAlign: 'center' as const,
marginBottom: '1rem',
color: '#333',
},
intro: {
fontSize: '1.125rem',
textAlign: 'center' as const,
color: '#666',
marginBottom: '3rem',
maxWidth: '700px',
margin: '0 auto 3rem',
},
alternativeContact: {
marginTop: '3rem',
textAlign: 'center' as const,
},
alternativeText: {
fontSize: '1rem',
color: '#666',
},
emailLink: {
color: '#667eea',
textDecoration: 'none',
fontWeight: 600,
},
};
export default Contact;
💡 Form Validation Patterns
- Real-time validation: Errors clear as user types
- Blur validation: Check field when user leaves it
- Submit validation: Final check before submission
- Visual feedback: Red borders for errors
- Disabled state: Prevent multiple submissions
- Success state: Show confirmation message
✅ Contact Form Complete!
Your contact section now has:
- ✅ Fully validated contact form
- ✅ Real-time error messages
- ✅ Professional success state
- ✅ Disabled state during submission
- ✅ Alternative contact method
- ✅ Type-safe form handling
🦶 Footer Component
Let's finish with a professional footer containing social links, copyright information, and any additional navigation.
Create Footer Component
src/components/Footer.tsx
import React from 'react';
import { socialLinks } from '../data/portfolio';
const Footer: React.FC = () => {
const currentYear = new Date().getFullYear();
return (
<footer style={styles.footer}>
<div style={styles.container}>
{/* Social Links */}
<div style={styles.socialSection}>
<h3 style={styles.socialHeading}>Connect With Me</h3>
<div style={styles.socialLinks}>
{socialLinks.map((link) => (
<a
key={link.platform}
href={link.url}
target="_blank"
rel="noopener noreferrer"
style={styles.socialLink}
aria-label={link.platform}
>
<span style={styles.socialIcon}>{link.icon}</span>
<span>{link.platform}</span>
</a>
))}
</div>
</div>
{/* Divider */}
<div style={styles.divider} />
{/* Copyright */}
<div style={styles.copyright}>
<p style={styles.copyrightText}>
© {currentYear} Your Name. All rights reserved.
</p>
<p style={styles.builtWith}>
Built with React & TypeScript
</p>
</div>
</div>
</footer>
);
};
const styles = {
footer: {
backgroundColor: '#1a1a1a',
color: '#ffffff',
padding: '3rem 2rem 2rem',
},
container: {
maxWidth: '1200px',
margin: '0 auto',
},
socialSection: {
textAlign: 'center' as const,
marginBottom: '2rem',
},
socialHeading: {
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '1.5rem',
color: '#ffffff',
},
socialLinks: {
display: 'flex',
justifyContent: 'center',
gap: '2rem',
flexWrap: 'wrap' as const,
},
socialLink: {
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
color: '#ffffff',
textDecoration: 'none',
fontSize: '1.125rem',
transition: 'color 0.2s ease',
},
socialIcon: {
fontSize: '1.5rem',
},
divider: {
height: '1px',
backgroundColor: '#444',
margin: '2rem 0',
},
copyright: {
textAlign: 'center' as const,
},
copyrightText: {
fontSize: '1rem',
color: '#ccc',
marginBottom: '0.5rem',
},
builtWith: {
fontSize: '0.875rem',
color: '#888',
},
};
export default Footer;
✅ Footer Complete!
Your footer now includes:
- ✅ Social media links with icons
- ✅ Dynamic copyright year
- ✅ Professional dark styling
- ✅ Accessibility attributes
- ✅ Responsive layout
🎉 Complete Solution
Now let's bring everything together! Here's the complete App.tsx that assembles all your components into a working portfolio.
Main App Component
src/App.tsx
import React from 'react';
import Navigation from './components/Navigation';
import Hero from './components/Hero';
import About from './components/About';
import Skills from './components/Skills';
import Projects from './components/Projects';
import Contact from './components/Contact';
import Footer from './components/Footer';
import './App.css';
function App() {
return (
<div className="App">
<Navigation />
<main>
<Hero />
<About />
<Skills />
<Projects />
<Contact />
</main>
<Footer />
</div>
);
}
export default App;
Basic Global Styles
src/App.css
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
}
html {
scroll-behavior: smooth;
}
/* Ensure sections have proper spacing with fixed nav */
section {
scroll-margin-top: 80px;
}
/* Link hover effects */
a {
transition: color 0.2s ease;
}
a:hover {
opacity: 0.8;
}
/* Button hover effects */
button:hover:not(:disabled) {
opacity: 0.9;
}
/* Input focus styles */
input:focus,
textarea:focus {
outline: none;
border-color: #667eea !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Responsive Design */
@media (max-width: 768px) {
/* Hide navigation links on mobile - you can add a hamburger menu here */
nav ul {
display: none;
}
/* Adjust heading sizes for mobile */
h1 {
font-size: 2rem !important;
}
h2 {
font-size: 2rem !important;
}
/* Adjust hero section for mobile */
section#home {
padding: 1rem;
min-height: auto;
padding-top: 100px;
padding-bottom: 60px;
}
/* Stack flex items on mobile */
.about-content,
.button-group {
flex-direction: column;
align-items: center;
}
}
Complete File Structure
Your Final Project Structure
portfolio/
├── src/
│ ├── components/
│ │ ├── Navigation.tsx ✅
│ │ ├── Hero.tsx ✅
│ │ ├── About.tsx ✅
│ │ ├── Skills.tsx ✅
│ │ ├── SkillCard.tsx ✅
│ │ ├── Projects.tsx ✅
│ │ ├── ProjectCard.tsx ✅
│ │ ├── Contact.tsx ✅
│ │ ├── ContactForm.tsx ✅
│ │ └── Footer.tsx ✅
│ ├── types/
│ │ └── index.ts ✅
│ ├── data/
│ │ └── portfolio.ts ✅
│ ├── App.tsx ✅
│ ├── App.css ✅
│ ├── main.tsx ✅
│ └── vite-env.d.ts
├── public/
│ └── favicon.ico
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
Running Your Portfolio
Development Commands
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Type check
npm run type-check
🎊 Congratulations!
You've built a complete, professional portfolio website with:
- ✅ 10 React components with proper TypeScript typing
- ✅ Responsive design for all screen sizes
- ✅ Interactive forms with validation
- ✅ Smooth scrolling and animations
- ✅ Reusable component patterns
- ✅ Professional styling and UX
- ✅ Type-safe event handling
- ✅ Real-world portfolio you can deploy!
🚀 Enhancements & Extensions
Your portfolio is complete, but here are some ideas to take it to the next level! Try implementing these enhancements to practice your skills further.
Easy Enhancements (30-60 minutes each)
✨ Enhancement 1: Dark Mode Toggle
Add a theme switcher to toggle between light and dark modes.
💡 Implementation Hint
// Use useState to track theme
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// Create a theme toggle button in Navigation
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
// Pass theme to all sections and adjust colors accordingly
✨ Enhancement 2: Mobile Hamburger Menu
Add a responsive hamburger menu for mobile devices.
💡 Implementation Hint
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Add hamburger button (visible only on mobile)
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
style={{ display: 'none' }} // Show with media query
>
☰
</button>
// Conditionally render menu
{isMenuOpen && (
<div style={styles.mobileMenu}>
{/* Navigation links */}
</div>
)}
✨ Enhancement 3: Scroll-to-Top Button
Add a button that appears when scrolling down and returns to the top.
💡 Implementation Hint
const [showScrollTop, setShowScrollTop] = useState(false);
useEffect(() => {
const handleScroll = () => {
setShowScrollTop(window.scrollY > 300);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Render button conditionally
{showScrollTop && (
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
style={styles.scrollTopButton}
>
↑
</button>
)}
Medium Enhancements (1-2 hours each)
🎨 Enhancement 4: Animation on Scroll
Add animations that trigger when sections scroll into view.
💡 Implementation Hint
Use the Intersection Observer API to detect when elements enter the viewport:
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
}
});
},
{ threshold: 0.1 }
);
const sections = document.querySelectorAll('section');
sections.forEach(section => observer.observe(section));
return () => observer.disconnect();
}, []);
🎨 Enhancement 5: Project Filtering
Add category filters to the projects section.
💡 Implementation Hint
const [filter, setFilter] = useState<string>('all');
const categories = ['all', 'web', 'mobile', 'api'];
const filteredProjects = projects.filter(project =>
filter === 'all' || project.category === filter
);
// Render filter buttons
{categories.map(cat => (
<button
onClick={() => setFilter(cat)}
style={{
...styles.filterButton,
backgroundColor: filter === cat ? '#667eea' : '#fff'
}}
>
{cat}
</button>
))}
🎨 Enhancement 6: Testimonials Section
Add a new section for client testimonials or recommendations.
💡 Implementation Hint
interface Testimonial {
id: number;
name: string;
role: string;
company: string;
text: string;
avatar?: string;
}
const TestimonialCard: React.FC<{ testimonial: Testimonial }> = ({ testimonial }) => {
return (
<div style={styles.testimonialCard}>
<p style={styles.quote}>"{testimonial.text}"</p>
<p style={styles.author}>
<strong>{testimonial.name}</strong>
<br />
{testimonial.role} at {testimonial.company}
</p>
</div>
);
};
Advanced Enhancements (2-4 hours each)
⚡ Enhancement 7: Blog Section
Add a blog section with articles about your development journey.
- Create a BlogPost interface and BlogCard component
- Display blog previews with read more links
- Add filtering by category or tags
- Include reading time estimation
⚡ Enhancement 8: Backend Integration
Connect your contact form to a real backend or email service.
- Use EmailJS or similar service for sending emails
- Or create a simple API endpoint with Node.js/Express
- Add proper error handling for failed submissions
- Store submissions in a database (optional)
⚡ Enhancement 9: CMS Integration
Use a headless CMS to manage your content dynamically.
- Set up Contentful, Sanity, or Strapi
- Fetch projects and blog posts from the CMS
- Update content without redeploying
- Add image optimization
💡 Challenge Yourself!
Pick 2-3 enhancements that interest you most and implement them. These additions will:
- Deepen your React and TypeScript skills
- Make your portfolio stand out
- Give you more features to discuss in interviews
- Provide practice with real-world scenarios
🌐 Deployment & Next Steps
Your portfolio is ready to share with the world! Let's deploy it and discuss what comes next in your React journey.
Deploying Your Portfolio
Option 1: Vercel (Recommended)
Vercel offers the easiest deployment for React applications.
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel
# Follow the prompts to deploy your project
Or use the Vercel Dashboard:
- Push your code to GitHub
- Go to vercel.com
- Import your GitHub repository
- Vercel auto-detects Vite and deploys!
Option 2: Netlify
Another excellent option with drag-and-drop deployment.
# Build your project
npm run build
# The dist/ folder contains your production build
Deploy via Netlify:
- Go to netlify.com
- Drag and drop your
distfolder - Or connect your GitHub repo for automatic deployments
Option 3: GitHub Pages
Free hosting directly from your GitHub repository.
# Install gh-pages
npm install --save-dev gh-pages
# Add to package.json scripts:
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
}
# Update vite.config.ts base URL:
export default defineConfig({
base: '/your-repo-name/',
// ... rest of config
})
# Deploy
npm run deploy
Before You Deploy - Checklist
| Task | Status |
|---|---|
| Update all placeholder text with your information | ⬜ |
| Replace sample projects with your actual projects | ⬜ |
| Add your real social media links | ⬜ |
| Test all links and navigation | ⬜ |
| Add a professional photo (optional) | ⬜ |
| Test on mobile devices | ⬜ |
| Check for TypeScript errors | ⬜ |
| Update favicon and title | ⬜ |
| Add meta tags for SEO | ⬜ |
| Test contact form functionality | ⬜ |
What You've Learned
Module 2 Recap
Throughout this module project, you've applied:
From Lesson 2.1 (Intro to React):
- Component-based architecture
- Functional components
- Project structure and organization
From Lesson 2.2 (JSX and TSX):
- JSX syntax and expressions
- Conditional rendering
- Mapping over arrays to render lists
- TypeScript in JSX
From Lesson 2.3 (Components and Props):
- Creating reusable components (SkillCard, ProjectCard)
- Defining and typing props with interfaces
- Component composition
- Props destructuring
From Lesson 2.4 (Styling):
- Inline styles with TypeScript
- Style objects and dynamic styling
- Responsive design principles
- Professional color schemes and layouts
From Lesson 2.5 (Events):
- Event handlers (onClick, onChange, onSubmit)
- Form handling and validation
- Typing events with TypeScript
- Controlled components
Next Steps in Your Journey
🚀 Where to Go From Here
Module 3: State and Interactivity
- Learn the useState hook in depth
- Master state management patterns
- Build complex interactive applications
- Understand lifting state up
Keep Building:
- Add the enhancements from Section 12
- Build variations of your portfolio
- Create portfolios for friends or family
- Experiment with different designs
Share Your Work:
- Deploy your portfolio and share the link
- Add it to your resume and LinkedIn
- Ask for feedback from other developers
- Use it in job applications
✅ Project Complete!
Congratulations! You've built a professional portfolio from scratch!
You now have:
- ✅ A fully functional, professional portfolio website
- ✅ Experience with React component architecture
- ✅ TypeScript skills for type-safe development
- ✅ Understanding of modern styling approaches
- ✅ Knowledge of form handling and validation
- ✅ A real project to showcase your skills
- ✅ Confidence to build more React applications
Great work! You're ready for Module 3! 🎉