🎨 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
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
💡 What this scaffolds in 2026
The react-ts template gives you a fully configured, modern stack out of the box, so you can focus on components instead of tooling:
- React 19 — the current major version, with the automatic JSX runtime enabled
- TypeScript 5.x — wired up with a sensible
tsconfig.json - Vite — instant dev server and lightning-fast hot reload
Make sure you're on Node.js 20 LTS or newer (22 LTS recommended). Check with node --version — Vite's current releases require it.
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
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:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff style C fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff style D fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff
💡 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
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;
Typed Props in Action
Because SkillCard declares skill: Skill, TypeScript checks every object you pass to it against the Skill interface — before the app ever runs. Click each example to see whether it satisfies the interface, and why. This is exactly the safety net that keeps a growing component tree from drifting out of sync with its data.
<SkillCard skill={/* must match interface Skill */} />
👆 Click a value above to see what TypeScript thinks.
💡 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
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
Notice there's no import React from 'react' here. Since React 17, and by default in every React 19 + Vite project, JSX is transformed by the automatic runtime, so you only import React when you actually reference the namespace (for example React.FC or React.FormEvent). Our component files import it because they use those types; App doesn't, so it stays clean.
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;
The Entry Point
src/main.tsx
Vite already generated this file when you scaffolded the project — it's what mounts your App into the page. This is the modern React 19 pattern: createRoot from react-dom/client, wrapped in <StrictMode> so React surfaces potential problems during development.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
// The '!' tells TypeScript this element definitely exists in index.html
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
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! 🎉