Welcome to your final day of Week 1! Today we're building a professional team directory that will teach you how to work with lists of data - one of the most common tasks in web development. You'll learn how React efficiently handles arrays and why keys are so important.
What we're building today: A searchable team member directory with filtering by department, contact information, and professional profiles.
By the end of this lesson, you will:
- Understand how to work with arrays of data in React
- Master the
.map()function for rendering lists of components - Learn why React needs keys and how to use them properly
- Implement search and filtering functionality
- Create responsive card layouts with CSS Grid
- Build a complete, professional-looking team directory
- Understand performance implications of list rendering
π’ Real-world analogy: Think of a company directory:
- You have a list of employees (array of objects)
- Each employee has the same type of information (name, role, department, photo)
- You want to display all employees in a consistent format
- You might want to filter by department or search by name
- When new employees join, they get added to the directory
In React applications, you'll constantly work with:
- Lists of products in an e-commerce site
- Social media posts in a feed
- Comments on a blog post
- Menu items in a navigation
- Todo items in a task manager
- Students in a classroom roster
π The .map() function is like a factory assembly line:
- Input: Raw materials (array of data)
- Process: Assembly instructions (function that transforms each item)
- Output: Finished products (array of React components)
// Example: Transform data into components
const numbers = [1, 2, 3]
const doubled = numbers.map(num => num * 2) // [2, 4, 6]
const people = [{name: "Alice"}, {name: "Bob"}]
const greetings = people.map(person => `Hello, ${person.name}!`) // ["Hello, Alice!", "Hello, Bob!"]π·οΈ Keys are like name tags at a conference:
- Without name tags: Hard to track who's who when people move around
- With name tags: Easy to identify individuals even if they change seats
- React: Uses keys to efficiently update the DOM when lists change
πΈ CHECKPOINT: Think of 3 websites you use that display lists of data. What kind of information do they show?
Open your terminal and create a new React project:
# Create new project for today
npm create vite@latest team-directory-app -- --template react
# Navigate to project
cd team-directory-app
# Install dependencies
npm install
# Start development server
npm run devπΈ CHECKPOINT: Your browser should show the default Vite + React page at http://localhost:5173/
In VS Code, create these folders in your src directory:
src/
βββ components/
βββ styles/
βββ data/
βββ utils/
Replace everything in src/App.jsx with:
import './App.css'
function App() {
return (
<div className="app">
<h1>Team Member Directory</h1>
<p>Today we're mastering arrays, map functions, and keys in React!</p>
</div>
)
}
export default AppReplace everything in src/App.css with:
/* Global App Styles */
* {
margin: 0; /* Removes default margins */
padding: 0; /* Removes default padding */
box-sizing: border-box; /* Makes width calculations easier */
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Clean, modern font */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* Purple gradient */
min-height: 100vh; /* Full screen height */
padding: 1rem; /* Small padding around edges */
}
.app {
max-width: 1400px; /* Limits width on very large screens */
margin: 0 auto; /* Centers the app */
background: rgba(255, 255, 255, 0.95); /* Semi-transparent white */
border-radius: 20px; /* Rounded corners */
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); /* Drop shadow */
padding: 2rem; /* Internal spacing */
min-height: 90vh; /* Minimum height */
backdrop-filter: blur(10px); /* Blur effect behind the app */
}
.app h1 {
color: #2d3436; /* Dark gray text */
margin-bottom: 0.5rem; /* Space below title */
font-size: 2.5rem; /* Large title */
text-align: center; /* Centers title */
text-shadow: 2px 2px 4px rgba(0,0,0,0.1); /* Subtle shadow */
background: linear-gradient(135deg, #667eea, #764ba2); /* Gradient text */
-webkit-background-clip: text; /* Clips background to text */
-webkit-text-fill-color: transparent; /* Makes text transparent to show gradient */
background-clip: text; /* Standard property */
}
.app p {
text-align: center; /* Centers description */
color: #636e72; /* Gray text */
margin-bottom: 2rem; /* Space below description */
font-size: 1.1rem; /* Slightly larger text */
font-style: italic; /* Italicized description */
}πΈ CHECKPOINT: Save and see your updated app with the new gradient text effect and semi-transparent background.
Let's create realistic team member data. Create a new file: src/data/teamMembers.js
// Team Member Data - In a real app, this might come from a database or API
export const teamMembers = [
{
id: 1, // Unique identifier
name: "Sarah Johnson", // Full name
role: "Frontend Developer", // Job title
department: "Engineering", // Department
email: "sarah.johnson@company.com", // Contact email
phone: "(555) 123-4567", // Phone number
location: "New York, NY", // Work location
avatar: "π©βπ»", // Emoji avatar (we'll use emojis for simplicity)
bio: "Passionate about creating beautiful, accessible user interfaces. Loves React and has 5 years of experience in frontend development.",
skills: ["React", "JavaScript", "CSS", "TypeScript", "Figma"], // Array of skills
joinDate: "2021-03-15", // When they joined the company
isActive: true // Current employment status
},
{
id: 2,
name: "Michael Chen",
role: "Backend Developer",
department: "Engineering",
email: "michael.chen@company.com",
phone: "(555) 234-5678",
location: "San Francisco, CA",
avatar: "π¨βπ»",
bio: "Expert in server-side technologies and database design. Enjoys solving complex problems and optimizing system performance.",
skills: ["Node.js", "Python", "PostgreSQL", "AWS", "Docker"],
joinDate: "2020-07-22",
isActive: true
},
{
id: 3,
name: "Emily Rodriguez",
role: "UX Designer",
department: "Design",
email: "emily.rodriguez@company.com",
phone: "(555) 345-6789",
location: "Austin, TX",
avatar: "π¨",
bio: "Creative problem solver who bridges the gap between user needs and business goals. Specializes in user research and interaction design.",
skills: ["Figma", "Adobe XD", "User Research", "Prototyping", "Wireframing"],
joinDate: "2021-11-08",
isActive: true
},
{
id: 4,
name: "David Park",
role: "Product Manager",
department: "Product",
email: "david.park@company.com",
phone: "(555) 456-7890",
location: "Seattle, WA",
avatar: "π",
bio: "Strategic thinker who loves turning ideas into successful products. Experienced in agile methodologies and cross-functional team leadership.",
skills: ["Product Strategy", "Agile", "Analytics", "Roadmapping", "Stakeholder Management"],
joinDate: "2019-05-12",
isActive: true
},
{
id: 5,
name: "Lisa Thompson",
role: "Data Scientist",
department: "Analytics",
email: "lisa.thompson@company.com",
phone: "(555) 567-8901",
location: "Boston, MA",
avatar: "π",
bio: "Data enthusiast who finds insights in numbers. Specializes in machine learning and predictive analytics for business intelligence.",
skills: ["Python", "R", "Machine Learning", "SQL", "Tableau"],
joinDate: "2020-09-30",
isActive: true
},
{
id: 6,
name: "Alex Kumar",
role: "DevOps Engineer",
department: "Engineering",
email: "alex.kumar@company.com",
phone: "(555) 678-9012",
location: "Chicago, IL",
avatar: "βοΈ",
bio: "Infrastructure expert who ensures systems run smoothly and securely. Passionate about automation and continuous deployment.",
skills: ["AWS", "Docker", "Kubernetes", "Jenkins", "Terraform"],
joinDate: "2021-01-18",
isActive: true
},
{
id: 7,
name: "Jessica Wu",
role: "Marketing Manager",
department: "Marketing",
email: "jessica.wu@company.com",
phone: "(555) 789-0123",
location: "Los Angeles, CA",
avatar: "π’",
bio: "Creative marketer who builds brand awareness and drives customer engagement. Expert in digital marketing and content strategy.",
skills: ["Digital Marketing", "Content Strategy", "SEO", "Social Media", "Analytics"],
joinDate: "2020-12-03",
isActive: true
},
{
id: 8,
name: "Robert Davis",
role: "Sales Director",
department: "Sales",
email: "robert.davis@company.com",
phone: "(555) 890-1234",
location: "Miami, FL",
avatar: "πΌ",
bio: "Results-driven sales leader with a track record of exceeding targets. Builds strong relationships with clients and partners.",
skills: ["Sales Strategy", "Client Relations", "Negotiation", "CRM", "Team Leadership"],
joinDate: "2018-03-07",
isActive: true
},
{
id: 9,
name: "Maria Gonzalez",
role: "HR Specialist",
department: "Human Resources",
email: "maria.gonzalez@company.com",
phone: "(555) 901-2345",
location: "Denver, CO",
avatar: "π₯",
bio: "People-focused professional who helps create a positive work environment. Specializes in talent acquisition and employee development.",
skills: ["Recruitment", "Employee Relations", "Training", "Policy Development", "Conflict Resolution"],
joinDate: "2019-08-14",
isActive: true
},
{
id: 10,
name: "James Wilson",
role: "Quality Assurance Lead",
department: "Engineering",
email: "james.wilson@company.com",
phone: "(555) 012-3456",
location: "(Remote)",
avatar: "π",
bio: "Detail-oriented QA professional who ensures product quality and reliability. Expert in both manual and automated testing strategies.",
skills: ["Test Automation", "Manual Testing", "Selenium", "API Testing", "Bug Tracking"],
joinDate: "2020-04-25",
isActive: true
}
]
// Utility function to get unique departments
export const getDepartments = () => {
const departments = teamMembers.map(member => member.department)
return [...new Set(departments)].sort() // Remove duplicates and sort alphabetically
}
// Utility function to get team statistics
export const getTeamStats = () => {
return {
totalMembers: teamMembers.length,
departments: getDepartments().length,
activeMembers: teamMembers.filter(member => member.isActive).length,
averageSkills: Math.round(teamMembers.reduce((total, member) => total + member.skills.length, 0) / teamMembers.length)
}
}π§ Key Concepts in Our Data:
- Consistent structure: Every team member has the same properties
- Unique IDs: Essential for React keys
- Rich information: Multiple data types (strings, arrays, booleans)
- Utility functions: Helper functions to process the data
- Real-world relevance: Data structure similar to actual company directories
πΈ CHECKPOINT: Save the file. This data will power our entire application.
Create a new file: src/components/TeamMemberCard.jsx
import React from 'react'
// This component displays information for a single team member
// It receives all team member data through props
function TeamMemberCard(props) {
// Destructure props for easier access (this is a common pattern)
const {
name,
role,
department,
email,
phone,
location,
avatar,
bio,
skills,
joinDate,
isActive
} = props.member
// Format the join date to be more readable
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
// Calculate how long they've been with the company
const calculateTenure = (joinDate) => {
const join = new Date(joinDate)
const now = new Date()
const diffTime = Math.abs(now - join)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
const years = Math.floor(diffDays / 365)
const months = Math.floor((diffDays % 365) / 30)
if (years > 0) {
return `${years} year${years > 1 ? 's' : ''}, ${months} month${months !== 1 ? 's' : ''}`
} else {
return `${months} month${months !== 1 ? 's' : ''}`
}
}
return (
<div className={`team-member-card ${isActive ? 'active' : 'inactive'}`}>
{/* Header Section with Avatar and Basic Info */}
<div className="member-header">
<div className="avatar-section">
<span className="avatar">{avatar}</span>
<div className={`status-indicator ${isActive ? 'active' : 'inactive'}`}>
{isActive ? 'π’' : 'π΄'}
</div>
</div>
<div className="basic-info">
<h3 className="member-name">{name}</h3>
<p className="member-role">{role}</p>
<span className="member-department">{department}</span>
</div>
</div>
{/* Contact Information Section */}
<div className="contact-info">
<div className="contact-item">
<span className="contact-icon">π§</span>
<a href={`mailto:${email}`} className="contact-link">
{email}
</a>
</div>
<div className="contact-item">
<span className="contact-icon">π</span>
<a href={`tel:${phone}`} className="contact-link">
{phone}
</a>
</div>
<div className="contact-item">
<span className="contact-icon">π</span>
<span className="contact-text">{location}</span>
</div>
</div>
{/* Bio Section */}
<div className="bio-section">
<h4 className="section-title">About</h4>
<p className="bio-text">{bio}</p>
</div>
{/* Skills Section */}
<div className="skills-section">
<h4 className="section-title">Skills</h4>
<div className="skills-container">
{/* Here's the .map() in action! */}
{skills.map((skill, index) => (
<span key={index} className="skill-tag">
{skill}
</span>
))}
</div>
</div>
{/* Employment Info Section */}
<div className="employment-info">
<div className="employment-item">
<span className="employment-label">Joined:</span>
<span className="employment-value">{formatDate(joinDate)}</span>
</div>
<div className="employment-item">
<span className="employment-label">Tenure:</span>
<span className="employment-value">{calculateTenure(joinDate)}</span>
</div>
</div>
</div>
)
}
export default TeamMemberCardπ§ Key React Concepts Here:
- Props destructuring:
const { name, role } = props.membermakes code cleaner - Map function:
skills.map()creates a component for each skill - Key prop:
key={index}helps React track list items - Conditional classes:
className={isActive ? 'active' : 'inactive'} - Helper functions:
formatDate()andcalculateTenure()process data - Event handlers:
mailto:andtel:links for contact actions
πΈ CHECKPOINT: Save the file. We'll see this component in action soon!
Create a new file: src/styles/TeamMemberCard.css
/* Team Member Card Component Styles */
.team-member-card {
background: white; /* White card background */
border-radius: 20px; /* Rounded corners */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); /* Subtle shadow */
padding: 1.5rem; /* Internal spacing */
transition: all 0.3s ease; /* Smooth hover effects */
border: 2px solid transparent; /* Transparent border (will change on hover) */
height: fit-content; /* Adjusts height to content */
display: flex; /* Flex layout for sections */
flex-direction: column; /* Vertical layout */
gap: 1rem; /* Space between sections */
}
/* Card hover effects */
.team-member-card:hover {
transform: translateY(-5px); /* Lifts card on hover */
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); /* Stronger shadow */
border-color: #667eea; /* Blue border on hover */
}
/* Active vs inactive member styling */
.team-member-card.active {
border-left: 4px solid #00b894; /* Green left border for active members */
}
.team-member-card.inactive {
border-left: 4px solid #e17055; /* Red left border for inactive members */
opacity: 0.7; /* Slightly faded for inactive */
}
/* Member Header Section */
.member-header {
display: flex; /* Horizontal layout */
align-items: center; /* Centers items vertically */
gap: 1rem; /* Space between avatar and info */
padding-bottom: 1rem; /* Space below header */
border-bottom: 2px solid #f1f3f4; /* Subtle divider line */
position: relative; /* For positioning status indicator */
}
/* Avatar Section */
.avatar-section {
position: relative; /* For positioning status indicator */
display: flex; /* Centers avatar */
align-items: center; /* Centers vertically */
justify-content: center; /* Centers horizontally */
}
.avatar {
font-size: 4rem; /* Large emoji avatar */
background: linear-gradient(135deg, #667eea, #764ba2); /* Gradient background */
border-radius: 50%; /* Perfect circle */
width: 80px; /* Fixed width */
height: 80px; /* Fixed height */
display: flex; /* Centers emoji */
align-items: center; /* Centers vertically */
justify-content: center; /* Centers horizontally */
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); /* Blue shadow */
border: 3px solid white; /* White border around avatar */
}
.status-indicator {
position: absolute; /* Positions relative to avatar */
bottom: -2px; /* Near bottom of avatar */
right: -2px; /* Near right edge of avatar */
font-size: 1rem; /* Size of status emoji */
background: white; /* White background */
border-radius: 50%; /* Circular background */
padding: 2px; /* Small padding around emoji */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* Shadow for depth */
}
/* Basic Info Section */
.basic-info {
flex: 1; /* Takes up remaining space */
}
.member-name {
font-size: 1.4rem; /* Large name text */
font-weight: bold; /* Bold name */
color: #2d3436; /* Dark gray color */
margin: 0 0 0.3rem 0; /* Small margin below name */
line-height: 1.2; /* Tight line spacing */
}
.member-role {
font-size: 1rem; /* Normal role text size */
color: #636e72; /* Medium gray */
margin: 0 0 0.5rem 0; /* Small margin below role */
font-weight: 500; /* Medium weight */
}
.member-department {
background: linear-gradient(135deg, #74b9ff, #0984e3); /* Blue gradient */
color: white; /* White text */
padding: 0.3rem 0.8rem; /* Internal spacing */
border-radius: 15px; /* Rounded pill shape */
font-size: 0.8rem; /* Small text */
font-weight: 600; /* Semi-bold */
text-transform: uppercase; /* Uppercase text */
letter-spacing: 0.5px; /* Spaced letters */
display: inline-block; /* Allows padding */
}
/* Contact Information Section */
.contact-info {
display: flex; /* Vertical layout for contact items */
flex-direction: column; /* Stacks items vertically */
gap: 0.5rem; /* Space between contact items */
}
.contact-item {
display: flex; /* Horizontal layout for icon and text */
align-items: center; /* Centers items vertically */
gap: 0.5rem; /* Space between icon and text */
padding: 0.3rem 0; /* Small vertical padding */
}
.contact-icon {
font-size: 1rem; /* Icon size */
width: 20px; /* Fixed width for alignment */
text-align: center; /* Centers icon */
}
.contact-link {
color: #667eea; /* Blue link color */
text-decoration: none; /* Removes underline */
font-size: 0.9rem; /* Slightly smaller text */
transition: color 0.3s ease; /* Smooth color transition */
}
.contact-link:hover {
color: #764ba2; /* Darker blue on hover */
text-decoration: underline; /* Underline on hover */
}
.contact-text {
color: #636e72; /* Gray text for non-links */
font-size: 0.9rem; /* Slightly smaller text */
}
/* Bio Section */
.bio-section {
background: #f8f9fa; /* Light gray background */
border-radius: 10px; /* Rounded corners */
padding: 1rem; /* Internal spacing */
border-left: 3px solid #667eea; /* Blue left accent */
}
.section-title {
font-size: 0.9rem; /* Small section title */
color: #2d3436; /* Dark gray */
margin: 0 0 0.5rem 0; /* Small margin below title */
font-weight: 600; /* Semi-bold */
text-transform: uppercase; /* Uppercase titles */
letter-spacing: 0.5px; /* Spaced letters */
}
.bio-text {
font-size: 0.9rem; /* Readable bio text size */
color: #636e72; /* Medium gray */
line-height: 1.5; /* Good line spacing for readability */
margin: 0; /* No default margin */
}
/* Skills Section */
.skills-section {
/* Inherits default styles, no additional styling needed for container */
}
.skills-container {
display: flex; /* Horizontal layout for skills */
flex-wrap: wrap; /* Allows skills to wrap to next line */
gap: 0.5rem; /* Space between skill tags */
}
.skill-tag {
background: linear-gradient(135deg, #fd79a8, #fdcb6e); /* Pink to orange gradient */
color: white; /* White text */
padding: 0.3rem 0.8rem; /* Internal spacing */
border-radius: 15px; /* Rounded pill shape */
font-size: 0.8rem; /* Small text */
font-weight: 500; /* Medium weight */
border: 1px solid rgba(255, 255, 255, 0.2); /* Subtle border */
transition: transform 0.2s ease; /* Smooth hover effect */
}
.skill-tag:hover {
transform: scale(1.05); /* Slightly grows on hover */
cursor: default; /* Shows it's not clickable */
}
/* Employment Info Section */
.employment-info {
background: #f8f9fa; /* Light gray background */
border-radius: 10px; /* Rounded corners */
padding: 1rem; /* Internal spacing */
display: flex; /* Horizontal layout for employment items */
justify-content: space-between; /* Spreads items across width */
border-left: 3px solid #00b894; /* Green left accent */
}
.employment-item {
display: flex; /* Vertical layout for label and value */
flex-direction: column; /* Stacks label above value */
align-items: center; /* Centers content */
gap: 0.3rem; /* Space between label and value */
}
.employment-label {
font-size: 0.8rem; /* Small label text */
color: #636e72; /* Medium gray */
font-weight: 600; /* Semi-bold */
text-transform: uppercase; /* Uppercase labels */
letter-spacing: 0.5px; /* Spaced letters */
}
.employment-value {
font-size: 0.9rem; /* Value text size */
color: #2d3436; /* Dark gray */
font-weight: 500; /* Medium weight */
text-align: center; /* Centers multi-line text */
}
/* Responsive design for smaller screens */
@media (max-width: 768px) {
.member-header {
flex-direction: column; /* Stacks avatar and info vertically on mobile */
text-align: center; /* Centers content */
gap: 0.5rem; /* Less space between items */
}
.basic-info {
text-align: center; /* Centers text on mobile */
}
.employment-info {
flex-direction: column; /* Stacks employment items vertically */
gap: 1rem; /* Space between items */
}
.employment-item {
align-items: flex-start; /* Left-aligns items on mobile */
}
}
@media (max-width: 480px) {
.team-member-card {
padding: 1rem; /* Less padding on small screens */
}
.avatar {
font-size: 3rem; /* Smaller avatar on mobile */
width: 60px; /* Smaller width */
height: 60px; /* Smaller height */
}
.member-name {
font-size: 1.2rem; /* Smaller name on mobile */
}
}Update src/components/TeamMemberCard.jsx to import the CSS:
import React from 'react'
// Import our CSS file
import '../styles/TeamMemberCard.css'
function TeamMemberCard(props) {
// ... all existing code stays exactly the same
return (
<div className={`team-member-card ${isActive ? 'active' : 'inactive'}`}>
{/* All existing JSX stays unchanged */}
</div>
)
}
export default TeamMemberCardπΈ CHECKPOINT: Save the file. We still won't see the cards because we haven't used them in our App yet.
This is where we'll use the .map() function to display all team members. Create a new file: src/components/TeamDirectory.jsx
import React, { useState } from 'react'
import TeamMemberCard from './TeamMemberCard'
// This component manages the display and filtering of all team members
function TeamDirectory(props) {
// State for search functionality
const [searchTerm, setSearchTerm] = useState('')
const [selectedDepartment, setSelectedDepartment] = useState('All')
// Get the team members from props
const { members, departments } = props
// Filter members based on search term and selected department
const filteredMembers = members.filter(member => {
// Check if member matches search term (search in name, role, or skills)
const matchesSearch =
member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.role.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.skills.some(skill => skill.toLowerCase().includes(searchTerm.toLowerCase()))
// Check if member matches selected department
const matchesDepartment =
selectedDepartment === 'All' || member.department === selectedDepartment
// Member must match both search and department filters
return matchesSearch && matchesDepartment
})
// Handle search input changes
const handleSearchChange = (e) => {
setSearchTerm(e.target.value)
}
// Handle department filter changes
const handleDepartmentChange = (e) => {
setSelectedDepartment(e.target.value)
}
// Clear all filters
const clearFilters = () => {
setSearchTerm('')
setSelectedDepartment('All')
}
return (
<div className="team-directory">
{/* Directory Header */}
<div className="directory-header">
<h2 className="directory-title">
π₯ Our Amazing Team
</h2>
<p className="directory-subtitle">
Meet the talented people who make our company great!
</p>
</div>
{/* Search and Filter Controls */}
<div className="directory-controls">
<div className="search-section">
<div className="search-input-container">
<span className="search-icon">π</span>
<input
type="text"
placeholder="Search by name, role, or skills..."
value={searchTerm}
onChange={handleSearchChange}
className="search-input"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="clear-search-button"
title="Clear search"
>
β
</button>
)}
</div>
</div>
<div className="filter-section">
<label htmlFor="department-filter" className="filter-label">
Department:
</label>
<select
id="department-filter"
value={selectedDepartment}
onChange={handleDepartmentChange}
className="department-select"
>
<option value="All">All Departments</option>
{/* Use .map() to create an option for each department */}
{departments.map(department => (
<option key={department} value={department}>
{department}
</option>
))}
</select>
</div>
<div className="results-section">
<span className="results-count">
{filteredMembers.length} of {members.length} members
</span>
{(searchTerm || selectedDepartment !== 'All') && (
<button onClick={clearFilters} className="clear-filters-button">
Clear Filters
</button>
)}
</div>
</div>
{/* Team Members Grid - This is where the magic happens! */}
<div className="members-grid">
{filteredMembers.length > 0 ? (
// Use .map() to create a TeamMemberCard for each filtered member
filteredMembers.map(member => (
<TeamMemberCard
key={member.id} // IMPORTANT: Unique key for each member
member={member} // Pass entire member object as props
/>
))
) : (
// Show message when no members match filters
<div className="no-results">
<div className="no-results-icon">π</div>
<h3 className="no-results-title">No team members found</h3>
<p className="no-results-text">
Try adjusting your search terms or filters.
</p>
<button onClick={clearFilters} className="no-results-button">
Show All Members
</button>
</div>
)}
</div>
{/* Results Summary */}
{filteredMembers.length > 0 && (
<div className="results-summary">
<p className="summary-text">
Showing {filteredMembers.length} team member{filteredMembers.length !== 1 ? 's' : ''}
{searchTerm && ` matching "${searchTerm}"`}
{selectedDepartment !== 'All' && ` from ${selectedDepartment}`}
</p>
</div>
)}
</div>
)
}
export default TeamDirectoryπ§ Key React Concepts Here:
- Array filtering:
members.filter()creates new arrays based on conditions - Multiple filters: Combining search and department filters
- Conditional rendering: Different content based on whether results exist
- Map function:
filteredMembers.map()creates components from data - Keys importance:
key={member.id}helps React optimize list updates - Complex state: Managing search term and department filter separately
πΈ CHECKPOINT: Save the file. Next we'll style this component and connect it to our App.
Create a new file: src/styles/TeamDirectory.css
/* Team Directory Component Styles */
.team-directory {
width: 100%; /* Full width */
max-width: 1200px; /* Limits width on large screens */
margin: 0 auto; /* Centers directory */
}
/* Directory Header */
.directory-header {
text-align: center; /* Centers header content */
margin-bottom: 2rem; /* Space below header */
padding: 2rem; /* Internal spacing */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* Gradient background */
border-radius: 20px; /* Rounded corners */
color: white; /* White text */
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); /* Blue shadow */
}
.directory-title {
font-size: 2.5rem; /* Large title */
margin-bottom: 0.5rem; /* Space below title */
font-weight: bold; /* Bold text */
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); /* Text shadow */
}
.directory-subtitle {
font-size: 1.1rem; /* Subtitle size */
opacity: 0.9; /* Semi-transparent */
font-style: italic; /* Italicized */
margin: 0; /* No default margin */
}
/* Directory Controls Section */
.directory-controls {
display: flex; /* Horizontal layout */
flex-wrap: wrap; /* Allows wrapping on small screens */
gap: 1rem; /* Space between control sections */
align-items: center; /* Centers items vertically */
justify-content: space-between; /* Spreads sections across width */
background: white; /* White background */
padding: 1.5rem; /* Internal spacing */
border-radius: 15px; /* Rounded corners */
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); /* Subtle shadow */
margin-bottom: 2rem; /* Space below controls */
border: 2px solid #f1f3f4; /* Light border */
}
/* Search Section */
.search-section {
flex: 1; /* Takes up available space */
min-width: 250px; /* Minimum width for search */
}
.search-input-container {
position: relative; /* For positioning icons */
display: flex; /* Horizontal layout */
align-items: center; /* Centers items vertically */
}
.search-icon {
position: absolute; /* Positions inside input */
left: 12px; /* Left spacing */
font-size: 1rem; /* Icon size */
color: #636e72; /* Gray icon color */
pointer-events: none; /* Prevents clicking on icon */
z-index: 1; /* Above input background */
}
.search-input {
width: 100%; /* Full width of container */
padding: 0.8rem 0.8rem 0.8rem 2.5rem; /* Left padding for icon */
border: 2px solid #e9ecef; /* Light gray border */
border-radius: 25px; /* Rounded input */
font-size: 1rem; /* Input text size */
background: #f8f9fa; /* Light gray background */
transition: all 0.3s ease; /* Smooth focus effects */
color: #2d3436; /* Dark text */
}
.search-input:focus {
outline: none; /* Removes default browser outline */
border-color: #667eea; /* Blue border on focus */
background: white; /* White background on focus */
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); /* Focus glow */
}
.search-input::placeholder {
color: #636e72; /* Gray placeholder text */
opacity: 0.8; /* Semi-transparent */
}
.clear-search-button {
position: absolute; /* Positions inside input */
right: 8px; /* Right spacing */
background: none; /* No background */
border: none; /* No border */
font-size: 0.8rem; /* Small icon */
cursor: pointer; /* Shows it's clickable */
padding: 4px; /* Small padding for easier clicking */
border-radius: 50%; /* Circular button */
transition: background 0.3s ease; /* Smooth hover effect */
}
.clear-search-button:hover {
background: rgba(225, 112, 85, 0.1); /* Light red background on hover */
}
/* Filter Section */
.filter-section {
display: flex; /* Horizontal layout */
align-items: center; /* Centers items vertically */
gap: 0.5rem; /* Space between label and select */
}
.filter-label {
font-size: 0.9rem; /* Label text size */
font-weight: 600; /* Semi-bold */
color: #2d3436; /* Dark gray */
white-space: nowrap; /* Prevents label from wrapping */
}
.department-select {
padding: 0.6rem 1rem; /* Internal spacing */
border: 2px solid #e9ecef; /* Light border */
border-radius: 8px; /* Rounded corners */
font-size: 0.9rem; /* Select text size */
background: white; /* White background */
color: #2d3436; /* Dark text */
cursor: pointer; /* Shows it's interactive */
transition: border-color 0.3s ease; /* Smooth border transition */
min-width: 150px; /* Minimum select width */
}
.department-select:focus {
outline: none; /* Removes default outline */
border-color: #667eea; /* Blue border on focus */
}
/* Results Section */
.results-section {
display: flex; /* Horizontal layout */
align-items: center; /* Centers items vertically */
gap: 1rem; /* Space between count and button */
flex-wrap: wrap; /* Allows wrapping */
}
.results-count {
font-size: 0.9rem; /* Small text */
color: #636e72; /* Gray text */
font-weight: 500; /* Medium weight */
white-space: nowrap; /* Prevents wrapping */
}
.clear-filters-button {
background: linear-gradient(135deg, #fd79a8, #fdcb6e); /* Pink to orange gradient */
color: white; /* White text */
border: none; /* No border */
padding: 0.5rem 1rem; /* Internal spacing */
border-radius: 20px; /* Rounded button */
font-size: 0.8rem; /* Small button text */
font-weight: 600; /* Semi-bold */
cursor: pointer; /* Shows it's clickable */
transition: all 0.3s ease; /* Smooth hover effects */
text-transform: uppercase; /* Uppercase text */
letter-spacing: 0.5px; /* Spaced letters */
}
.clear-filters-button:hover {
transform: translateY(-2px); /* Lifts button on hover */
box-shadow: 0 4px 15px rgba(253, 121, 168, 0.4); /* Pink shadow */
}
/* Members Grid - This is where the cards are displayed */
.members-grid {
display: grid; /* CSS Grid layout */
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); /* Responsive columns */
gap: 2rem; /* Space between cards */
margin-bottom: 2rem; /* Space below grid */
}
/* No Results Section */
.no-results {
grid-column: 1 / -1; /* Spans all grid columns */
text-align: center; /* Centers content */
padding: 4rem 2rem; /* Large internal spacing */
background: white; /* White background */
border-radius: 20px; /* Rounded corners */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); /* Subtle shadow */
border: 2px dashed #e9ecef; /* Dashed border */
}
.no-results-icon {
font-size: 4rem; /* Large sad emoji */
margin-bottom: 1rem; /* Space below icon */
}
.no-results-title {
font-size: 1.5rem; /* Large title */
color: #2d3436; /* Dark gray */
margin-bottom: 0.5rem; /* Space below title */
font-weight: 600; /* Semi-bold */
}
.no-results-text {
color: #636e72; /* Gray text */
margin-bottom: 1.5rem; /* Space below text */
font-size: 1rem; /* Normal text size */
}
.no-results-button {
background: linear-gradient(135deg, #667eea, #764ba2); /* Blue gradient */
color: white; /* White text */
border: none; /* No border */
padding: 0.8rem 2rem; /* Internal spacing */
border-radius: 25px; /* Rounded button */
font-size: 1rem; /* Button text size */
font-weight: 600; /* Semi-bold */
cursor: pointer; /* Shows it's clickable */
transition: all 0.3s ease; /* Smooth hover effects */
}
.no-results-button:hover {
transform: translateY(-2px); /* Lifts button on hover */
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); /* Blue shadow */
}
/* Results Summary */
.results-summary {
text-align: center; /* Centers summary */
padding: 1rem; /* Internal spacing */
background: rgba(102, 126, 234, 0.1); /* Light blue background */
border-radius: 10px; /* Rounded corners */
border: 2px solid rgba(102, 126, 234, 0.2); /* Light blue border */
}
.summary-text {
color: #636e72; /* Gray text */
font-size: 0.9rem; /* Summary text size */
margin: 0; /* No default margin */
font-style: italic; /* Italicized summary */
}
/* Responsive Design */
@media (max-width: 768px) {
.directory-controls {
flex-direction: column; /* Stacks controls vertically on tablet */
align-items: stretch; /* Stretches items to full width */
gap: 1.5rem; /* More space between sections */
}
.search-section {
order: 1; /* Search first on mobile */
}
.filter-section {
order: 2; /* Filter second */
justify-content: center; /* Centers filter on mobile */
}
.results-section {
order: 3; /* Results last */
justify-content: center; /* Centers results section */
}
.members-grid {
grid-template-columns: 1fr; /* Single column on tablet */
gap: 1.5rem; /* Less gap on mobile */
}
.directory-title {
font-size: 2rem; /* Smaller title on mobile */
}
}
@media (max-width: 480px) {
.directory-header {
padding: 1.5rem; /* Less padding on small screens */
}
.directory-controls {
padding: 1rem; /* Less padding on mobile */
}
.search-input-container {
margin-bottom: 0.5rem; /* Space below search on mobile */
}
.members-grid {
gap: 1rem; /* Even less gap on small mobile */
}
.no-results {
padding: 2rem 1rem; /* Less padding in no results */
}
.no-results-icon {
font-size: 3rem; /* Smaller icon on mobile */
}
}First, import CSS in src/components/TeamDirectory.jsx:
import React, { useState } from 'react'
import TeamMemberCard from './TeamMemberCard'
// Import our CSS file
import '../styles/TeamDirectory.css'
function TeamDirectory(props) {
// ... all existing code stays exactly the same
}
export default TeamDirectoryNow update src/App.jsx to use our complete directory:
import './App.css'
// Import our components
import TeamDirectory from './components/TeamDirectory'
// Import our data and utility functions
import { teamMembers, getDepartments, getTeamStats } from './data/teamMembers'
function App() {
// Get team statistics
const teamStats = getTeamStats()
return (
<div className="app">
<h1>Team Member Directory</h1>
<p>
Browse our team of {teamStats.totalMembers} members across {teamStats.departments} departments!
</p>
{/* Team Statistics Summary */}
<div className="team-stats">
<div className="stat-item">
<span className="stat-number">{teamStats.totalMembers}</span>
<span className="stat-label">Team Members</span>
</div>
<div className="stat-item">
<span className="stat-number">{teamStats.departments}</span>
<span className="stat-label">Departments</span>
</div>
<div className="stat-item">
<span className="stat-number">{teamStats.activeMembers}</span>
<span className="stat-label">Active Members</span>
</div>
<div className="stat-item">
<span className="stat-number">{teamStats.averageSkills}</span>
<span className="stat-label">Avg Skills</span>
</div>
</div>
{/* Main Team Directory Component */}
<TeamDirectory
members={teamMembers}
departments={getDepartments()}
/>
</div>
)
}
export default AppAdd these styles to the end of your src/App.css file:
/* Team Statistics Section */
.team-stats {
display: flex; /* Horizontal layout for stats */
justify-content: center; /* Centers stats */
gap: 2rem; /* Space between stat items */
margin: 2rem 0; /* Space above and below stats */
padding: 1.5rem; /* Internal spacing */
background: rgba(255, 255, 255, 0.8); /* Semi-transparent white */
border-radius: 15px; /* Rounded corners */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); /* Subtle shadow */
backdrop-filter: blur(10px); /* Blur effect */
}
.stat-item {
display: flex; /* Vertical layout for number and label */
flex-direction: column; /* Stacks number above label */
align-items: center; /* Centers content */
text-align: center; /* Centers text */
min-width: 100px; /* Minimum width for consistency */
}
.stat-number {
font-size: 2rem; /* Large number text */
font-weight: bold; /* Bold numbers */
color: #667eea; /* Blue color */
line-height: 1; /* Tight line spacing */
margin-bottom: 0.5rem; /* Space below number */
}
.stat-label {
font-size: 0.9rem; /* Small label text */
color: #636e72; /* Gray color */
font-weight: 500; /* Medium weight */
text-transform: uppercase; /* Uppercase labels */
letter-spacing: 0.5px; /* Spaced letters */
}
/* Responsive stats */
@media (max-width: 768px) {
.team-stats {
gap: 1rem; /* Less gap on mobile */
padding: 1rem; /* Less padding on mobile */
}
.stat-number {
font-size: 1.5rem; /* Smaller numbers on mobile */
}
.stat-label {
font-size: 0.8rem; /* Smaller labels on mobile */
}
}
@media (max-width: 480px) {
.team-stats {
flex-wrap: wrap; /* Allows wrapping on very small screens */
justify-content: center; /* Centers wrapped items */
}
.stat-item {
min-width: 80px; /* Smaller minimum width */
}
}πΈ CHECKPOINT: Save all files and check your browser! You should now see:
- Team statistics at the top
- A search bar and department filter
- A beautiful grid of team member cards
- Responsive design that works on mobile
Try these interactions to see React's array handling in action:
-
Search Functionality:
- Type "sarah" in the search box β Should show only Sarah Johnson
- Type "react" β Should show members with React skills
- Type "frontend" β Should show frontend developers
-
Department Filtering:
- Select "Engineering" β Should show only engineering team members
- Select "Design" β Should show only designers
-
Combined Filtering:
- Search "python" AND select "Engineering" β Should show backend engineers
- Try different combinations
-
Clear Filters:
- Add some filters, then click "Clear Filters"
- Should reset to show all members
-
No Results:
- Search for "xyz123" β Should show the "no results" message
πΈ CHECKPOINT: Test each of these scenarios and take screenshots showing the filtering working correctly.
π Factory Assembly Line Analogy:
// Raw materials (data)
const teamMembers = [
{ id: 1, name: "Sarah", role: "Developer" },
{ id: 2, name: "Mike", role: "Designer" },
{ id: 3, name: "Lisa", role: "Manager" }
]
// Assembly instructions (transform function)
const createCard = (member) => <TeamMemberCard key={member.id} member={member} />
// Finished products (React components)
const memberCards = teamMembers.map(createCard)β Without Keys (React gets confused):
// BAD: No keys
{members.map(member =>
<TeamMemberCard member={member} /> // React doesn't know which is which!
)}β With Keys (React is efficient):
// GOOD: Unique keys
{members.map(member =>
<TeamMemberCard key={member.id} member={member} /> // React tracks each card!
)}Why Keys Matter:
- Performance: React can efficiently update only changed items
- State preservation: Component state stays with the right component
- Animations: Smooth transitions when items are added/removed
- Bug prevention: Prevents components from displaying wrong data
| Method | Purpose | Example |
|---|---|---|
.map() |
Transform each item | members.map(m => <Card member={m} />) |
.filter() |
Keep only matching items | members.filter(m => m.department === 'Engineering') |
.some() |
Check if any item matches | skills.some(skill => skill.includes('React')) |
.reduce() |
Calculate single value | members.reduce((total, m) => total + m.skills.length, 0) |
Try adding some custom team members to see how React handles dynamic data:
Add this function to your src/data/teamMembers.js:
// Function to add new team members (you can use this to test)
export const addTeamMember = (newMember) => {
const memberWithId = {
...newMember,
id: teamMembers.length + 1 // Simple ID generation
}
teamMembers.push(memberWithId)
return memberWithId
}Create a few team members for people you know:
// Add after the existing team members array
const customMembers = [
{
id: 11,
name: "Your Friend's Name",
role: "Student",
department: "Education",
email: "friend@email.com",
phone: "(555) 111-2222",
location: "Your City",
avatar: "π",
bio: "Learning React and loving it!",
skills: ["HTML", "CSS", "JavaScript", "Learning React"],
joinDate: "2025-01-01",
isActive: true
}
// Add more custom members here
]
// Add them to the main array
teamMembers.push(...customMembers)React's Optimization with Keys:
- When you filter the list, React doesn't recreate all components
- It reuses existing components and only updates what changed
- Keys help React identify which components can be reused
Performance Tips You've Learned:
- Always use unique, stable keys (never use array index for dynamic lists)
- Filter data before mapping to components
- Keep component logic simple for better re-render performance
πΈ CHECKPOINT: Add at least 2 custom team members and test that they appear in search and filtering.
- β
Mastered the
.map()function for rendering lists - β Learned why React keys are crucial for performance
- β Built a complete searchable and filterable directory
- β Implemented complex array filtering with multiple criteria
- β Created responsive CSS Grid layouts
- β Combined multiple React concepts (state, props, components, arrays)
- β Built a professional-quality application with real-world functionality
-
Array Rendering:
.map()transforms data arrays into component arrays- Keys help React efficiently update lists
- Filter before mapping for better performance
-
Search & Filtering:
- Multiple filter criteria can be combined
- State manages current filter values
- Conditional rendering shows different content based on results
-
Component Architecture:
- Parent components manage data and filters
- Child components focus on display
- Props pass both data and callback functions
Today's patterns are used everywhere:
- E-commerce: Product listings with search and filters
- Social Media: Posts, comments, friend lists
- Admin Dashboards: User management, data tables
- Content Management: Article lists, media galleries
- Mobile Apps: Contact lists, message threads
Next week we'll build a Personal Finance Tracker where you'll learn:
- Complex state management - Objects and arrays in state
- Form handling - Adding and removing items dynamically
- Data calculations - Running totals and statistics
- Local storage - Persisting data between sessions
- Add more team members: Include friends, family, or fictional characters
- Add more search fields: What about searching by location or skills?
- Add sorting: Sort by name, join date, or department alphabetically
- Add favorites: Let users mark favorite team members
- Add contact actions: Make the email and phone links work better
- Virtual scrolling: For very large lists (1000+ items)
- Debounced search: Prevent too many searches while typing
- Lazy loading: Load more items as user scrolls
- Memoization: Optimize expensive calculations with useMemo
- Custom hooks: Extract search/filter logic into reusable hooks
- Missing keys warning: Always provide unique keys for list items
- Filter not working: Check that filter logic matches your data structure
- Search not working: Make sure you're converting to lowercase for comparison
- Components not updating: Verify that state is being updated correctly
- CSS Grid issues: Use browser DevTools to inspect grid layout
- Performance problems: Check if you're filtering efficiently before mapping
- Always use keys:
key={item.id}notkey={index} - Keep transforms simple: Complex logic should be in separate functions
- Filter before mapping: Don't create components you won't show
- Use meaningful names:
members.map(member => ...)notarr.map(x => ...)
πΈ FINAL CHECKPOINT: Take a screenshot of your completed team directory showing:
- All team members displayed in a responsive grid
- Search functionality working (try searching for a name)
- Department filter working (select a specific department)
- Results count updating correctly
- Mobile-responsive design (try resizing your browser)
Congratulations! You've successfully completed Week 1 of React development! You now have a solid foundation in:
- Component creation and composition
- Props and state management
- Event handling and user interactions
- Array manipulation and list rendering
- Search and filtering functionality
- Responsive design principles
- Professional code organization
This team directory demonstrates enterprise-level React patterns that you'll use throughout your development career. You're now ready to tackle more complex applications in Week 2!
- Day 1: Personal Business Card (Components, JSX, Styling)
- Day 2: Interactive Recipe Card (State, Events, Conditional Rendering)
- Day 3: Grade Calculator (Props, Forms, Calculations)
- Day 4: Team Directory (Arrays, Map, Search/Filter)
You now have 4 professional React applications in your portfolio that showcase fundamental React skills to potential employers or clients!