In this hands-on lab, you will build a comprehensive employee directory that demonstrates dynamic list rendering, React keys, filtering, and sorting. You'll learn how to efficiently render large datasets, implement search functionality, and optimize list performance. This lab covers the essential patterns for working with dynamic data in React applications.
Total Estimated Time: 30 minutes
Estimated Time: 8 minutes
In this exercise, you will create a new React project for the employee directory and set up the basic structure for rendering lists of employees. You'll learn about the map() function for rendering arrays, the importance of React keys, and basic list rendering patterns.
- Open VS Code on your Windows virtual machine
- Open the integrated terminal (`Ctrl + ``)
- Navigate to your desired project directory:
cd Desktop - Create a new React application called "employee-directory":
npx create-react-app employee-directory
- Navigate into the project directory:
cd employee-directory - Open the project in VS Code (File > Open Folder and select
employee-directory)
- Start the development server:
npm start
- Verify the application loads in your browser at
http://localhost:3000
- Create a
datafolder in thesrcdirectory - Create a new file called
employeeData.jsin thedatafolder - Add the following employee data:
export const employees = [ { id: 1, firstName: "Sarah", lastName: "Johnson", email: "sarah.johnson@company.com", phone: "(555) 123-4567", department: "Engineering", position: "Senior Software Engineer", location: "New York", hireDate: "2021-03-15", salary: 95000, profileImage: "👩💻", skills: ["JavaScript", "React", "Node.js", "Python"], status: "Active" }, { id: 2, firstName: "Michael", lastName: "Chen", email: "michael.chen@company.com", phone: "(555) 234-5678", department: "Marketing", position: "Marketing Manager", location: "San Francisco", hireDate: "2020-07-22", salary: 78000, profileImage: "👨💼", skills: ["Digital Marketing", "Analytics", "Content Strategy", "SEO"], status: "Active" }, { id: 3, firstName: "Emily", lastName: "Rodriguez", email: "emily.rodriguez@company.com", phone: "(555) 345-6789", department: "Design", position: "UX Designer", location: "Austin", hireDate: "2022-01-10", salary: 72000, profileImage: "👩🎨", skills: ["Figma", "User Research", "Prototyping", "Adobe Creative Suite"], status: "Active" }, { id: 4, firstName: "David", lastName: "Thompson", email: "david.thompson@company.com", phone: "(555) 456-7890", department: "Engineering", position: "DevOps Engineer", location: "Seattle", hireDate: "2019-11-05", salary: 88000, profileImage: "👨🔧", skills: ["AWS", "Docker", "Kubernetes", "CI/CD"], status: "Active" }, { id: 5, firstName: "Lisa", lastName: "Wang", email: "lisa.wang@company.com", phone: "(555) 567-8901", department: "Sales", position: "Sales Director", location: "Chicago", hireDate: "2018-05-14", salary: 105000, profileImage: "👩💼", skills: ["Sales Strategy", "CRM", "Team Leadership", "Business Development"], status: "Active" }, { id: 6, firstName: "James", lastName: "Wilson", email: "james.wilson@company.com", phone: "(555) 678-9012", department: "HR", position: "HR Specialist", location: "Denver", hireDate: "2021-09-03", salary: 58000, profileImage: "👨💻", skills: ["Recruitment", "Employee Relations", "Policy Development", "Training"], status: "Active" }, { id: 7, firstName: "Amanda", lastName: "Brown", email: "amanda.brown@company.com", phone: "(555) 789-0123", department: "Engineering", position: "Frontend Developer", location: "Boston", hireDate: "2022-06-20", salary: 82000, profileImage: "👩💻", skills: ["React", "TypeScript", "CSS", "JavaScript"], status: "Active" }, { id: 8, firstName: "Robert", lastName: "Davis", email: "robert.davis@company.com", phone: "(555) 890-1234", department: "Finance", position: "Financial Analyst", location: "Miami", hireDate: "2020-12-08", salary: 68000, profileImage: "👨💼", skills: ["Financial Modeling", "Excel", "SQL", "Data Analysis"], status: "Active" }, { id: 9, firstName: "Jennifer", lastName: "Martinez", email: "jennifer.martinez@company.com", phone: "(555) 901-2345", department: "Design", position: "Graphic Designer", location: "Los Angeles", hireDate: "2021-04-12", salary: 65000, profileImage: "👩🎨", skills: ["Photoshop", "Illustrator", "Brand Design", "Print Design"], status: "On Leave" }, { id: 10, firstName: "Christopher", lastName: "Taylor", email: "christopher.taylor@company.com", phone: "(555) 012-3456", department: "Operations", position: "Operations Manager", location: "Phoenix", hireDate: "2019-08-18", salary: 85000, profileImage: "👨💼", skills: ["Process Improvement", "Project Management", "Supply Chain", "Analytics"], status: "Active" } ]; export const departments = [ "All Departments", "Engineering", "Marketing", "Design", "Sales", "HR", "Finance", "Operations" ]; export const locations = [ "All Locations", "New York", "San Francisco", "Austin", "Seattle", "Chicago", "Denver", "Boston", "Miami", "Los Angeles", "Phoenix" ];
- Create a
componentsfolder in thesrcdirectory - Create
EmployeeCard.jsin thecomponentsfolder:import React from 'react'; import './EmployeeCard.css'; function EmployeeCard({ employee }) { const formatSalary = (salary) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(salary); }; const formatHireDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); }; const getStatusClass = (status) => { return status.toLowerCase().replace(' ', '-'); }; return ( <div className="employee-card"> <div className="employee-header"> <div className="profile-section"> <span className="profile-image">{employee.profileImage}</span> <div className="name-section"> <h3 className="employee-name"> {employee.firstName} {employee.lastName} </h3> <p className="employee-position">{employee.position}</p> </div> </div> <div className={`status-badge ${getStatusClass(employee.status)}`}> {employee.status} </div> </div> <div className="employee-details"> <div className="detail-row"> <span className="detail-label">Department:</span> <span className="detail-value">{employee.department}</span> </div> <div className="detail-row"> <span className="detail-label">Location:</span> <span className="detail-value">{employee.location}</span> </div> <div className="detail-row"> <span className="detail-label">Email:</span> <span className="detail-value">{employee.email}</span> </div> <div className="detail-row"> <span className="detail-label">Phone:</span> <span className="detail-value">{employee.phone}</span> </div> <div className="detail-row"> <span className="detail-label">Hire Date:</span> <span className="detail-value">{formatHireDate(employee.hireDate)}</span> </div> <div className="detail-row"> <span className="detail-label">Salary:</span> <span className="detail-value salary">{formatSalary(employee.salary)}</span> </div> </div> <div className="skills-section"> <span className="skills-label">Skills:</span> <div className="skills-list"> {employee.skills.map((skill, index) => ( <span key={index} className="skill-tag"> {skill} </span> ))} </div> </div> </div> ); } export default EmployeeCard;
- Create
EmployeeCard.cssin thecomponentsfolder:.employee-card { background: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); padding: 1.5rem; margin-bottom: 1rem; transition: transform 0.2s ease, box-shadow 0.2s ease; } .employee-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12); } .employee-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; } .profile-section { display: flex; align-items: center; gap: 1rem; } .profile-image { font-size: 3rem; display: flex; align-items: center; justify-content: center; width: 60px; height: 60px; background: #f8f9fa; border-radius: 50%; } .name-section { display: flex; flex-direction: column; } .employee-name { font-size: 1.4rem; color: #2c3e50; margin: 0 0 0.3rem 0; font-weight: 600; } .employee-position { color: #6c757d; margin: 0; font-size: 1rem; font-weight: 500; } .status-badge { padding: 0.4rem 0.8rem; border-radius: 20px; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; } .status-badge.active { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .status-badge.on-leave { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } .status-badge.inactive { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .employee-details { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; margin-bottom: 1.5rem; } .detail-row { display: flex; flex-direction: column; gap: 0.2rem; } .detail-label { font-size: 0.85rem; color: #6c757d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .detail-value { color: #2c3e50; font-weight: 500; } .detail-value.salary { color: #28a745; font-weight: 600; } .skills-section { border-top: 1px solid #e9ecef; padding-top: 1rem; } .skills-label { font-size: 0.85rem; color: #6c757d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 0.8rem; } .skills-list { display: flex; flex-wrap: wrap; gap: 0.5rem; } .skill-tag { background: #e7f3ff; color: #0056b3; padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.8rem; font-weight: 500; border: 1px solid #b3d7ff; } @media (max-width: 768px) { .employee-header { flex-direction: column; gap: 1rem; align-items: stretch; } .employee-details { grid-template-columns: 1fr; } .profile-section { justify-content: center; text-align: center; } .name-section { align-items: center; } }
- Replace the contents of
src/App.jswith:import React, { useState } from 'react'; import EmployeeCard from './components/EmployeeCard'; import { employees } from './data/employeeData'; import './App.css'; function App() { const [employeeList, setEmployeeList] = useState(employees); return ( <div className="App"> <div className="employee-directory"> <header className="directory-header"> <h1>Employee Directory</h1> <p>Find and connect with your colleagues</p> <div className="stats"> <div className="stat-item"> <span className="stat-number">{employeeList.length}</span> <span className="stat-label">Total Employees</span> </div> <div className="stat-item"> <span className="stat-number"> {employeeList.filter(emp => emp.status === 'Active').length} </span> <span className="stat-label">Active</span> </div> <div className="stat-item"> <span className="stat-number"> {new Set(employeeList.map(emp => emp.department)).size} </span> <span className="stat-label">Departments</span> </div> </div> </header> <main className="directory-content"> <div className="employees-grid"> {employeeList.map(employee => ( <EmployeeCard key={employee.id} employee={employee} /> ))} </div> {employeeList.length === 0 && ( <div className="no-employees"> <h3>No employees found</h3> <p>Try adjusting your search or filter criteria.</p> </div> )} </main> </div> </div> ); } export default App;
- Replace the contents of
src/App.csswith:* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; line-height: 1.6; } .App { min-height: 100vh; } .employee-directory { max-width: 1200px; margin: 0 auto; padding: 2rem; } .directory-header { text-align: center; margin-bottom: 3rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 3rem 2rem; border-radius: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } .directory-header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } .directory-header p { font-size: 1.2rem; opacity: 0.9; margin-bottom: 2rem; } .stats { display: flex; justify-content: center; gap: 3rem; } .stat-item { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; } .stat-number { font-size: 2rem; font-weight: bold; color: #ffeb3b; } .stat-label { font-size: 0.9rem; opacity: 0.8; text-transform: uppercase; letter-spacing: 0.5px; } .directory-content { background: white; border-radius: 20px; padding: 2rem; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08); } .employees-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem; } .no-employees { text-align: center; padding: 4rem 2rem; color: #6c757d; } .no-employees h3 { font-size: 1.5rem; margin-bottom: 1rem; color: #495057; } @media (max-width: 768px) { .employee-directory { padding: 1rem; } .directory-header { padding: 2rem 1rem; } .directory-header h1 { font-size: 2rem; } .stats { flex-direction: column; gap: 1rem; } .employees-grid { grid-template-columns: 1fr; } .directory-content { padding: 1rem; } }
- Save all files and view your employee directory in the browser
- Verify that all 10 employees are displayed in a responsive grid
- Check that each employee card shows all the required information
- Test the responsive design by resizing the browser window
- Open browser console and verify there are no key warnings
Exercise 1 Summary: You have successfully created an employee directory with basic list rendering using the map() function. You learned how to structure complex data, create reusable card components, implement proper React keys for list items, and handle responsive grid layouts. The directory displays employee information in an organized, visually appealing format with proper component composition.
Estimated Time: 10 minutes
In this exercise, you will implement search and filtering functionality for the employee directory. You'll learn how to filter arrays based on user input, implement real-time search, and create dropdown filters for departments and locations. This demonstrates dynamic list manipulation and state-driven filtering.
-
Create
SearchBar.jsin thecomponentsfolder:import React from 'react'; import './SearchBar.css'; function SearchBar({ searchTerm, setSearchTerm, placeholder = "Search employees..." }) { const handleSearchChange = (e) => { setSearchTerm(e.target.value); }; const clearSearch = () => { setSearchTerm(''); }; return ( <div className="search-bar"> <div className="search-input-container"> <span className="search-icon">🔍</span> <input type="text" className="search-input" placeholder={placeholder} value={searchTerm} onChange={handleSearchChange} /> {searchTerm && ( <button className="clear-search" onClick={clearSearch}> ✕ </button> )} </div> {searchTerm && ( <p className="search-hint"> Searching for "{searchTerm}" in names, positions, departments, and skills </p> )} </div> ); } export default SearchBar;
-
Create
SearchBar.cssin thecomponentsfolder:.search-bar { margin-bottom: 1.5rem; } .search-input-container { position: relative; display: flex; align-items: center; } .search-icon { position: absolute; left: 1rem; font-size: 1.2rem; color: #6c757d; z-index: 1; } .search-input { width: 100%; padding: 1rem 1rem 1rem 3rem; border: 2px solid #e9ecef; border-radius: 25px; font-size: 1rem; transition: border-color 0.3s ease, box-shadow 0.3s ease; background: white; } .search-input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .clear-search { position: absolute; right: 1rem; background: #6c757d; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.8rem; transition: background-color 0.3s ease; } .clear-search:hover { background: #495057; } .search-hint { margin-top: 0.5rem; font-size: 0.9rem; color: #6c757d; text-align: center; font-style: italic; }
-
Create
FilterControls.jsin thecomponentsfolder:import React from 'react'; import './FilterControls.css'; function FilterControls({ departments, locations, selectedDepartment, setSelectedDepartment, selectedLocation, setSelectedLocation, selectedStatus, setSelectedStatus, onClearFilters }) { const statusOptions = ['All Status', 'Active', 'On Leave', 'Inactive']; const hasActiveFilters = selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status'; return ( <div className="filter-controls"> <div className="filter-row"> <div className="filter-group"> <label htmlFor="department-filter">Department</label> <select id="department-filter" className="filter-select" value={selectedDepartment} onChange={(e) => setSelectedDepartment(e.target.value)} > {departments.map(department => ( <option key={department} value={department}> {department} </option> ))} </select> </div> <div className="filter-group"> <label htmlFor="location-filter">Location</label> <select id="location-filter" className="filter-select" value={selectedLocation} onChange={(e) => setSelectedLocation(e.target.value)} > {locations.map(location => ( <option key={location} value={location}> {location} </option> ))} </select> </div> <div className="filter-group"> <label htmlFor="status-filter">Status</label> <select id="status-filter" className="filter-select" value={selectedStatus} onChange={(e) => setSelectedStatus(e.target.value)} > {statusOptions.map(status => ( <option key={status} value={status}> {status} </option> ))} </select> </div> {hasActiveFilters && ( <div className="filter-group"> <label> </label> <button className="clear-filters-btn" onClick={onClearFilters} title="Clear all filters" > Clear Filters </button> </div> )} </div> {hasActiveFilters && ( <div className="active-filters"> <span className="active-filters-label">Active filters:</span> <div className="filter-tags"> {selectedDepartment !== 'All Departments' && ( <span className="filter-tag"> Department: {selectedDepartment} <button onClick={() => setSelectedDepartment('All Departments')}>✕</button> </span> )} {selectedLocation !== 'All Locations' && ( <span className="filter-tag"> Location: {selectedLocation} <button onClick={() => setSelectedLocation('All Locations')}>✕</button> </span> )} {selectedStatus !== 'All Status' && ( <span className="filter-tag"> Status: {selectedStatus} <button onClick={() => setSelectedStatus('All Status')}>✕</button> </span> )} </div> </div> )} </div> ); } export default FilterControls;
-
Create
FilterControls.cssin thecomponentsfolder:.filter-controls { background: #f8f9fa; padding: 1.5rem; border-radius: 12px; margin-bottom: 2rem; border: 1px solid #e9ecef; } .filter-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; align-items: end; } .filter-group { display: flex; flex-direction: column; gap: 0.5rem; } .filter-group label { font-weight: 600; color: #495057; font-size: 0.9rem; } .filter-select { padding: 0.75rem; border: 2px solid #e9ecef; border-radius: 8px; font-size: 1rem; background: white; cursor: pointer; transition: border-color 0.3s ease; } .filter-select:focus { outline: none; border-color: #667eea; } .clear-filters-btn { background: #dc3545; color: white; border: none; padding: 0.75rem 1rem; border-radius: 8px; cursor: pointer; font-weight: 600; transition: background-color 0.3s ease; white-space: nowrap; } .clear-filters-btn:hover { background: #c82333; } .active-filters { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6; } .active-filters-label { font-weight: 600; color: #495057; margin-right: 1rem; } .filter-tags { display: inline-flex; flex-wrap: wrap; gap: 0.5rem; } .filter-tag { background: #667eea; color: white; padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.8rem; display: flex; align-items: center; gap: 0.5rem; } .filter-tag button { background: rgba(255, 255, 255, 0.2); border: none; color: white; border-radius: 50%; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.7rem; } .filter-tag button:hover { background: rgba(255, 255, 255, 0.3); } @media (max-width: 768px) { .filter-row { grid-template-columns: 1fr; } .filter-tags { margin-top: 0.5rem; } }
- Update
src/App.jsto include search and filtering functionality:import React, { useState, useMemo } from 'react'; import EmployeeCard from './components/EmployeeCard'; import SearchBar from './components/SearchBar'; import FilterControls from './components/FilterControls'; import { employees, departments, locations } from './data/employeeData'; import './App.css'; function App() { // State for search and filters const [searchTerm, setSearchTerm] = useState(''); const [selectedDepartment, setSelectedDepartment] = useState('All Departments'); const [selectedLocation, setSelectedLocation] = useState('All Locations'); const [selectedStatus, setSelectedStatus] = useState('All Status'); // Filtered employee list using useMemo for performance const filteredEmployees = useMemo(() => { let filtered = employees; // Apply search filter if (searchTerm) { const searchLower = searchTerm.toLowerCase(); filtered = filtered.filter(employee => { const fullName = `${employee.firstName} ${employee.lastName}`.toLowerCase(); const position = employee.position.toLowerCase(); const department = employee.department.toLowerCase(); const skills = employee.skills.join(' ').toLowerCase(); const email = employee.email.toLowerCase(); return fullName.includes(searchLower) || position.includes(searchLower) || department.includes(searchLower) || skills.includes(searchLower) || email.includes(searchLower); }); } // Apply department filter if (selectedDepartment !== 'All Departments') { filtered = filtered.filter(employee => employee.department === selectedDepartment ); } // Apply location filter if (selectedLocation !== 'All Locations') { filtered = filtered.filter(employee => employee.location === selectedLocation ); } // Apply status filter if (selectedStatus !== 'All Status') { filtered = filtered.filter(employee => employee.status === selectedStatus ); } return filtered; }, [employees, searchTerm, selectedDepartment, selectedLocation, selectedStatus]); // Function to clear all filters const clearAllFilters = () => { setSearchTerm(''); setSelectedDepartment('All Departments'); setSelectedLocation('All Locations'); setSelectedStatus('All Status'); }; // Calculate statistics const stats = useMemo(() => { return { total: filteredEmployees.length, active: filteredEmployees.filter(emp => emp.status === 'Active').length, departments: new Set(filteredEmployees.map(emp => emp.department)).size }; }, [filteredEmployees]); return ( <div className="App"> <div className="employee-directory"> <header className="directory-header"> <h1>Employee Directory</h1> <p>Find and connect with your colleagues</p> <div className="stats"> <div className="stat-item"> <span className="stat-number">{stats.total}</span> <span className="stat-label"> {searchTerm || selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status' ? 'Filtered Results' : 'Total Employees'} </span> </div> <div className="stat-item"> <span className="stat-number">{stats.active}</span> <span className="stat-label">Active</span> </div> <div className="stat-item"> <span className="stat-number">{stats.departments}</span> <span className="stat-label">Departments</span> </div> </div> </header> <main className="directory-content"> {/* Search Bar */} <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} placeholder="Search by name, position, department, skills, or email..." /> {/* Filter Controls */} <FilterControls departments={departments} locations={locations} selectedDepartment={selectedDepartment} setSelectedDepartment={setSelectedDepartment} selectedLocation={selectedLocation} setSelectedLocation={setSelectedLocation} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} onClearFilters={clearAllFilters} /> {/* Results Summary */} {(searchTerm || selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status') && ( <div className="results-summary"> <p> Showing {filteredEmployees.length} of {employees.length} employees {searchTerm && ` matching "${searchTerm}"`} </p> </div> )} {/* Employee Grid */} <div className="employees-grid"> {filteredEmployees.map(employee => ( <EmployeeCard key={employee.id} employee={employee} /> ))} </div> {/* No Results Message */} {filteredEmployees.length === 0 && ( <div className="no-employees"> <h3>No employees found</h3> <p> {searchTerm ? `No employees match your search for "${searchTerm}".` : 'No employees match your current filters.' } </p> <button className="clear-all-btn" onClick={clearAllFilters}> Clear All Filters </button> </div> )} </main> </div> </div> ); } export default App;
- Add these styles to
src/App.css:/* Add these new styles to the existing App.css */ .results-summary { background: #e7f3ff; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; border-left: 4px solid #0056b3; } .results-summary p { color: #0056b3; font-weight: 600; margin: 0; } .clear-all-btn { background: #667eea; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 8px; cursor: pointer; font-weight: 600; margin-top: 1rem; transition: background-color 0.3s ease; } .clear-all-btn:hover { background: #5a67d8; } /* Update existing no-employees styles */ .no-employees { text-align: center; padding: 4rem 2rem; color: #6c757d; background: #f8f9fa; border-radius: 12px; margin-top: 2rem; } .no-employees h3 { font-size: 1.5rem; margin-bottom: 1rem; color: #495057; } .no-employees p { margin-bottom: 1.5rem; font-size: 1.1rem; }
-
Save all files and test the search and filter features:
Search Testing:
- Search by employee names (e.g., "Sarah", "Michael")
- Search by positions (e.g., "Engineer", "Manager")
- Search by departments (e.g., "Engineering", "Marketing")
- Search by skills (e.g., "React", "JavaScript")
- Search by email domains (e.g., "@company.com")
- Test partial matches and case insensitivity
Filter Testing:
- Filter by different departments
- Filter by different locations
- Filter by status (Active, On Leave)
- Combine multiple filters
- Use active filter tags to remove individual filters
Performance Testing:
- Notice how the statistics update in real-time
- Observe the filtered results count
- Test the "Clear All Filters" functionality
-
Verify the following behaviors:
- Search is case-insensitive and matches partial text
- Filters can be combined and work together
- Active filter tags appear when filters are applied
- Statistics update to reflect filtered results
- Clear buttons work for individual filters and all filters
- No results message appears when no employees match
Exercise 2 Summary: You have successfully implemented comprehensive search and filtering functionality for the employee directory. You learned how to filter arrays based on multiple criteria, implement real-time search across multiple fields, use useMemo for performance optimization, create interactive filter controls, and provide clear user feedback about active filters and results. The filtering system demonstrates advanced array manipulation and state-driven UI updates.
Estimated Time: 12 minutes
In this exercise, you will implement sorting functionality for the employee list and add performance optimizations for large datasets. You'll learn how to sort arrays by different criteria, implement dynamic sort controls, and optimize list rendering performance using React keys and memoization techniques.
-
Create
SortControls.jsin thecomponentsfolder:import React from 'react'; import './SortControls.css'; function SortControls({ sortBy, setSortBy, sortOrder, setSortOrder }) { const sortOptions = [ { value: 'name', label: 'Name' }, { value: 'position', label: 'Position' }, { value: 'department', label: 'Department' }, { value: 'location', label: 'Location' }, { value: 'hireDate', label: 'Hire Date' }, { value: 'salary', label: 'Salary' } ]; const handleSortChange = (newSortBy) => { if (sortBy === newSortBy) { // If clicking the same sort option, toggle order setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { // If clicking a different sort option, set new sort and default to ascending setSortBy(newSortBy); setSortOrder('asc'); } }; const getSortIcon = (optionValue) => { if (sortBy !== optionValue) { return '↕️'; // Neutral sort icon } return sortOrder === 'asc' ? '↑' : '↓'; }; const getSortButtonClass = (optionValue) => { return `sort-button ${sortBy === optionValue ? 'active' : ''}`; }; return ( <div className="sort-controls"> <div className="sort-header"> <h4>Sort by:</h4> <span className="sort-hint"> Click again to reverse order </span> </div> <div className="sort-buttons"> {sortOptions.map(option => ( <button key={option.value} className={getSortButtonClass(option.value)} onClick={() => handleSortChange(option.value)} title={`Sort by ${option.label} ${ sortBy === option.value ? (sortOrder === 'asc' ? '(click for descending)' : '(click for ascending)') : '(ascending)' }`} > <span className="sort-label">{option.label}</span> <span className="sort-icon">{getSortIcon(option.value)}</span> </button> ))} </div> <div className="current-sort"> Currently sorted by: <strong>{sortOptions.find(opt => opt.value === sortBy)?.label}</strong> {' '}({sortOrder === 'asc' ? 'A-Z' : 'Z-A'}) </div> </div> ); } export default SortControls;
-
Create
SortControls.cssin thecomponentsfolder:.sort-controls { background: white; padding: 1.5rem; border-radius: 12px; margin-bottom: 1.5rem; border: 1px solid #e9ecef; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .sort-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .sort-header h4 { color: #2c3e50; margin: 0; font-size: 1.1rem; } .sort-hint { font-size: 0.85rem; color: #6c757d; font-style: italic; } .sort-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; } .sort-button { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; border: 2px solid #e9ecef; background: white; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; font-weight: 500; } .sort-button:hover { border-color: #667eea; background: #f8f9ff; transform: translateY(-1px); } .sort-button.active { background: #667eea; border-color: #667eea; color: white; } .sort-button.active:hover { background: #5a67d8; border-color: #5a67d8; } .sort-label { font-weight: 600; } .sort-icon { font-size: 0.9rem; font-weight: bold; } .current-sort { font-size: 0.9rem; color: #495057; padding: 0.8rem; background: #f8f9fa; border-radius: 6px; text-align: center; } .current-sort strong { color: #667eea; } @media (max-width: 768px) { .sort-header { flex-direction: column; gap: 0.5rem; align-items: flex-start; } .sort-buttons { justify-content: center; } .sort-button { flex: 1; min-width: 120px; justify-content: center; } }
-
Create
PerformanceMonitor.jsin thecomponentsfolder:import React, { useState, useEffect } from 'react'; import './PerformanceMonitor.css'; function PerformanceMonitor({ employeeCount, renderTime }) { const [isVisible, setIsVisible] = useState(false); const [renderCount, setRenderCount] = useState(0); useEffect(() => { setRenderCount(prev => prev + 1); }); const toggleVisibility = () => { setIsVisible(!isVisible); }; const getPerformanceStatus = () => { if (renderTime < 10) return 'excellent'; if (renderTime < 50) return 'good'; if (renderTime < 100) return 'fair'; return 'poor'; }; return ( <div className="performance-monitor"> <button className="performance-toggle" onClick={toggleVisibility} title="Toggle performance metrics" > ⚡ Performance </button> {isVisible && ( <div className="performance-details"> <div className="performance-header"> <h5>Rendering Performance</h5> <button className="close-btn" onClick={() => setIsVisible(false)} > ✕ </button> </div> <div className="performance-metrics"> <div className="metric"> <span className="metric-label">Items Rendered:</span> <span className="metric-value">{employeeCount}</span> </div> <div className="metric"> <span className="metric-label">Render Time:</span> <span className={`metric-value ${getPerformanceStatus()}`}> {renderTime.toFixed(2)}ms </span> </div> <div className="metric"> <span className="metric-label">Total Renders:</span> <span className="metric-value">{renderCount}</span> </div> <div className="metric"> <span className="metric-label">Status:</span> <span className={`metric-value status-${getPerformanceStatus()}`}> {getPerformanceStatus().toUpperCase()} </span> </div> </div> <div className="performance-tips"> <p><strong>Performance Tips:</strong></p> <ul> <li>React keys help optimize re-renders</li> <li>useMemo prevents unnecessary calculations</li> <li>Proper component structure improves performance</li> </ul> </div> </div> )} </div> ); } export default PerformanceMonitor;
-
Create
PerformanceMonitor.cssin thecomponentsfolder:.performance-monitor { position: fixed; top: 20px; right: 20px; z-index: 1000; } .performance-toggle { background: #28a745; color: white; border: none; padding: 0.5rem 1rem; border-radius: 20px; cursor: pointer; font-size: 0.9rem; font-weight: 600; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); transition: all 0.3s ease; } .performance-toggle:hover { background: #218838; transform: translateY(-1px); } .performance-details { position: absolute; top: 100%; right: 0; margin-top: 0.5rem; background: white; border: 1px solid #e9ecef; border-radius: 12px; padding: 1rem; min-width: 280px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); animation: slideDown 0.3s ease; } @keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .performance-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #e9ecef; } .performance-header h5 { margin: 0; color: #2c3e50; } .close-btn { background: none; border: none; font-size: 1rem; cursor: pointer; color: #6c757d; padding: 0.2rem; } .close-btn:hover { color: #495057; } .performance-metrics { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; } .metric { display: flex; justify-content: space-between; align-items: center; } .metric-label { font-size: 0.9rem; color: #6c757d; } .metric-value { font-weight: 600; font-size: 0.9rem; } .metric-value.excellent { color: #28a745; } .metric-value.good { color: #ffc107; } .metric-value.fair { color: #fd7e14; } .metric-value.poor { color: #dc3545; } .status-excellent { color: #28a745; } .status-good { color: #ffc107; } .status-fair { color: #fd7e14; } .status-poor { color: #dc3545; } .performance-tips { font-size: 0.8rem; color: #6c757d; background: #f8f9fa; padding: 0.8rem; border-radius: 6px; } .performance-tips p { margin: 0 0 0.5rem 0; font-weight: 600; } .performance-tips ul { margin: 0; padding-left: 1rem; } .performance-tips li { margin-bottom: 0.2rem; }
- Update
src/App.jsto include sorting functionality:import React, { useState, useMemo } from 'react'; import EmployeeCard from './components/EmployeeCard'; import SearchBar from './components/SearchBar'; import FilterControls from './components/FilterControls'; import SortControls from './components/SortControls'; import PerformanceMonitor from './components/PerformanceMonitor'; import { employees, departments, locations } from './data/employeeData'; import './App.css'; function App() { // Existing state const [searchTerm, setSearchTerm] = useState(''); const [selectedDepartment, setSelectedDepartment] = useState('All Departments'); const [selectedLocation, setSelectedLocation] = useState('All Locations'); const [selectedStatus, setSelectedStatus] = useState('All Status'); // New sorting state const [sortBy, setSortBy] = useState('name'); const [sortOrder, setSortOrder] = useState('asc'); // Performance monitoring const [renderTime, setRenderTime] = useState(0); // Enhanced filtered and sorted employee list const processedEmployees = useMemo(() => { const startTime = performance.now(); let filtered = employees; // Apply search filter if (searchTerm) { const searchLower = searchTerm.toLowerCase(); filtered = filtered.filter(employee => { const fullName = `${employee.firstName} ${employee.lastName}`.toLowerCase(); const position = employee.position.toLowerCase(); const department = employee.department.toLowerCase(); const skills = employee.skills.join(' ').toLowerCase(); const email = employee.email.toLowerCase(); return fullName.includes(searchLower) || position.includes(searchLower) || department.includes(searchLower) || skills.includes(searchLower) || email.includes(searchLower); }); } // Apply department filter if (selectedDepartment !== 'All Departments') { filtered = filtered.filter(employee => employee.department === selectedDepartment ); } // Apply location filter if (selectedLocation !== 'All Locations') { filtered = filtered.filter(employee => employee.location === selectedLocation ); } // Apply status filter if (selectedStatus !== 'All Status') { filtered = filtered.filter(employee => employee.status === selectedStatus ); } // Apply sorting const sorted = [...filtered].sort((a, b) => { let aValue, bValue; switch (sortBy) { case 'name': aValue = `${a.firstName} ${a.lastName}`.toLowerCase(); bValue = `${b.firstName} ${b.lastName}`.toLowerCase(); break; case 'position': aValue = a.position.toLowerCase(); bValue = b.position.toLowerCase(); break; case 'department': aValue = a.department.toLowerCase(); bValue = b.department.toLowerCase(); break; case 'location': aValue = a.location.toLowerCase(); bValue = b.location.toLowerCase(); break; case 'hireDate': aValue = new Date(a.hireDate); bValue = new Date(b.hireDate); break; case 'salary': aValue = a.salary; bValue = b.salary; break; default: aValue = a.id; bValue = b.id; } if (aValue < bValue) { return sortOrder === 'asc' ? -1 : 1; } if (aValue > bValue) { return sortOrder === 'asc' ? 1 : -1; } return 0; }); const endTime = performance.now(); setRenderTime(endTime - startTime); return sorted; }, [employees, searchTerm, selectedDepartment, selectedLocation, selectedStatus, sortBy, sortOrder]); // Function to clear all filters and sorting const clearAllFilters = () => { setSearchTerm(''); setSelectedDepartment('All Departments'); setSelectedLocation('All Locations'); setSelectedStatus('All Status'); setSortBy('name'); setSortOrder('asc'); }; // Calculate statistics const stats = useMemo(() => { return { total: processedEmployees.length, active: processedEmployees.filter(emp => emp.status === 'Active').length, departments: new Set(processedEmployees.map(emp => emp.department)).size }; }, [processedEmployees]); return ( <div className="App"> {/* Performance Monitor */} <PerformanceMonitor employeeCount={processedEmployees.length} renderTime={renderTime} /> <div className="employee-directory"> <header className="directory-header"> <h1>Employee Directory</h1> <p>Find and connect with your colleagues</p> <div className="stats"> <div className="stat-item"> <span className="stat-number">{stats.total}</span> <span className="stat-label"> {searchTerm || selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status' ? 'Filtered Results' : 'Total Employees'} </span> </div> <div className="stat-item"> <span className="stat-number">{stats.active}</span> <span className="stat-label">Active</span> </div> <div className="stat-item"> <span className="stat-number">{stats.departments}</span> <span className="stat-label">Departments</span> </div> </div> </header> <main className="directory-content"> {/* Search Bar */} <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} placeholder="Search by name, position, department, skills, or email..." /> {/* Filter Controls */} <FilterControls departments={departments} locations={locations} selectedDepartment={selectedDepartment} setSelectedDepartment={setSelectedDepartment} selectedLocation={selectedLocation} setSelectedLocation={setSelectedLocation} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} onClearFilters={clearAllFilters} /> {/* Sort Controls */} <SortControls sortBy={sortBy} setSortBy={setSortBy} sortOrder={sortOrder} setSortOrder={setSortOrder} /> {/* Results Summary */} {(searchTerm || selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status') && ( <div className="results-summary"> <p> Showing {processedEmployees.length} of {employees.length} employees {searchTerm && ` matching "${searchTerm}"`} {' '}sorted by {sortBy} ({sortOrder === 'asc' ? 'ascending' : 'descending'}) </p> </div> )} {/* Employee Grid */} <div className="employees-grid"> {processedEmployees.map(employee => ( <EmployeeCard key={employee.id} employee={employee} /> ))} </div> {/* No Results Message */} {processedEmployees.length === 0 && ( <div className="no-employees"> <h3>No employees found</h3> <p> {searchTerm ? `No employees match your search for "${searchTerm}".` : 'No employees match your current filters.' } </p> <button className="clear-all-btn" onClick={clearAllFilters}> Clear All Filters & Reset Sort </button> </div> )} </main> </div> </div> ); } export default App;
-
Save all files and test the sorting functionality:
Sorting Tests:
- Click each sort button to sort by different criteria
- Click the same sort button twice to toggle between ascending/descending
- Observe the sort icons changing to show current sort direction
- Notice the "Currently sorted by" indicator updates
- Test sorting combined with filters and search
Performance Tests:
- Click the "⚡ Performance" button in the top-right corner
- Observe render times as you apply different filters and sorts
- Notice the render count increasing with each interaction
- Try rapid filter changes to see performance impact
-
Verify the following behaviors:
- Name sorting works alphabetically (first name + last name)
- Date sorting works chronologically (hire dates)
- Salary sorting works numerically
- Sort order toggles correctly when clicking the same option
- Performance monitor shows realistic metrics
- Render times should be under 50ms for good performance
-
Test edge cases:
- Sort with no results (empty filtered list)
- Sort with only one employee showing
- Apply multiple filters then sort
- Clear all filters and verify sort is reset
Exercise 3 Summary: You have successfully implemented comprehensive sorting functionality and performance monitoring for the employee directory. You learned how to sort arrays by multiple criteria (strings, numbers, dates), create interactive sort controls with visual feedback, implement performance monitoring to track render times, use useMemo for optimization, and provide clear user feedback about current sort state. The sorting system works seamlessly with existing filters and search, demonstrating advanced list manipulation and performance optimization techniques.
Estimated Time: 10 minutes
In this exercise, you will implement advanced list features including pagination, bulk actions, and virtualization concepts. You'll learn how to handle large datasets efficiently, implement user-friendly navigation controls, and optimize performance for real-world applications with hundreds or thousands of items.
-
Create
Pagination.jsin thecomponentsfolder:import React from 'react'; import './Pagination.css'; function Pagination({ currentPage, totalItems, itemsPerPage, onPageChange, onItemsPerPageChange }) { const totalPages = Math.ceil(totalItems / itemsPerPage); const startItem = (currentPage - 1) * itemsPerPage + 1; const endItem = Math.min(currentPage * itemsPerPage, totalItems); const getPageNumbers = () => { const pages = []; const maxVisiblePages = 5; if (totalPages <= maxVisiblePages) { // Show all pages if total pages is small for (let i = 1; i <= totalPages; i++) { pages.push(i); } } else { // Show smart pagination with ellipsis if (currentPage <= 3) { // Show first 4 pages + ellipsis + last page pages.push(1, 2, 3, 4, '...', totalPages); } else if (currentPage >= totalPages - 2) { // Show first page + ellipsis + last 4 pages pages.push(1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages); } else { // Show first page + ellipsis + current-1, current, current+1 + ellipsis + last page pages.push(1, '...', currentPage - 1, currentPage, currentPage + 1, '...', totalPages); } } return pages; }; const itemsPerPageOptions = [5, 10, 20, 50]; if (totalItems === 0) { return null; } return ( <div className="pagination"> <div className="pagination-info"> <span className="items-info"> Showing {startItem}-{endItem} of {totalItems} employees </span> <div className="items-per-page"> <label htmlFor="items-per-page">Show:</label> <select id="items-per-page" value={itemsPerPage} onChange={(e) => onItemsPerPageChange(Number(e.target.value))} className="items-select" > {itemsPerPageOptions.map(option => ( <option key={option} value={option}> {option} per page </option> ))} </select> </div> </div> {totalPages > 1 && ( <div className="pagination-controls"> <button className="page-btn" onClick={() => onPageChange(currentPage - 1)} disabled={currentPage === 1} title="Previous page" > ← Previous </button> <div className="page-numbers"> {getPageNumbers().map((page, index) => ( <span key={index}> {page === '...' ? ( <span className="ellipsis">...</span> ) : ( <button className={`page-number ${currentPage === page ? 'active' : ''}`} onClick={() => onPageChange(page)} > {page} </button> )} </span> ))} </div> <button className="page-btn" onClick={() => onPageChange(currentPage + 1)} disabled={currentPage === totalPages} title="Next page" > Next → </button> </div> )} </div> ); } export default Pagination;
-
Create
Pagination.cssin thecomponentsfolder:.pagination { background: white; padding: 1.5rem; border-radius: 12px; border: 1px solid #e9ecef; margin-top: 2rem; } .pagination-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid #e9ecef; } .items-info { color: #6c757d; font-weight: 500; } .items-per-page { display: flex; align-items: center; gap: 0.5rem; } .items-per-page label { font-weight: 600; color: #495057; } .items-select { padding: 0.4rem 0.8rem; border: 1px solid #e9ecef; border-radius: 6px; font-size: 0.9rem; cursor: pointer; } .pagination-controls { display: flex; justify-content: center; align-items: center; gap: 0.5rem; } .page-btn { padding: 0.6rem 1rem; border: 1px solid #e9ecef; background: white; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; } .page-btn:hover:not(:disabled) { background: #f8f9fa; border-color: #667eea; } .page-btn:disabled { opacity: 0.5; cursor: not-allowed; } .page-numbers { display: flex; gap: 0.2rem; } .page-number { width: 40px; height: 40px; border: 1px solid #e9ecef; background: white; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; } .page-number:hover { background: #f8f9fa; border-color: #667eea; } .page-number.active { background: #667eea; border-color: #667eea; color: white; } .ellipsis { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; color: #6c757d; font-weight: bold; } @media (max-width: 768px) { .pagination-info { flex-direction: column; gap: 1rem; align-items: stretch; text-align: center; } .pagination-controls { flex-wrap: wrap; gap: 0.3rem; } .page-btn { font-size: 0.9rem; padding: 0.5rem 0.8rem; } .page-number { width: 35px; height: 35px; font-size: 0.9rem; } }
-
Create
BulkActions.jsin thecomponentsfolder:import React, { useState } from 'react'; import './BulkActions.css'; function BulkActions({ selectedEmployees, onSelectAll, onDeselectAll, totalEmployees, onBulkExport, onBulkStatusChange }) { const [showActions, setShowActions] = useState(false); const selectedCount = selectedEmployees.length; const isAllSelected = selectedCount === totalEmployees && totalEmployees > 0; const isPartialSelected = selectedCount > 0 && selectedCount < totalEmployees; const handleSelectAll = () => { if (isAllSelected) { onDeselectAll(); } else { onSelectAll(); } }; const handleBulkAction = (action) => { switch (action) { case 'export': onBulkExport(selectedEmployees); break; case 'activate': onBulkStatusChange(selectedEmployees, 'Active'); break; case 'deactivate': onBulkStatusChange(selectedEmployees, 'Inactive'); break; default: break; } setShowActions(false); }; if (totalEmployees === 0) { return null; } return ( <div className="bulk-actions"> <div className="selection-controls"> <label className="select-all-container"> <input type="checkbox" checked={isAllSelected} ref={input => { if (input) input.indeterminate = isPartialSelected; }} onChange={handleSelectAll} className="select-all-checkbox" /> <span className="checkmark"></span> <span className="select-text"> {isAllSelected ? `All ${totalEmployees} selected` : selectedCount > 0 ? `${selectedCount} selected` : 'Select all' } </span> </label> {selectedCount > 0 && ( <button className="clear-selection" onClick={onDeselectAll} title="Clear selection" > Clear </button> )} </div> {selectedCount > 0 && ( <div className="bulk-action-controls"> <button className="bulk-actions-toggle" onClick={() => setShowActions(!showActions)} > Actions ({selectedCount}) {showActions ? '▼' : '▶'} </button> {showActions && ( <div className="bulk-actions-menu"> <button className="bulk-action-btn export" onClick={() => handleBulkAction('export')} > 📄 Export Selected </button> <button className="bulk-action-btn activate" onClick={() => handleBulkAction('activate')} > ✅ Mark as Active </button> <button className="bulk-action-btn deactivate" onClick={() => handleBulkAction('deactivate')} > ❌ Mark as Inactive </button> </div> )} </div> )} </div> ); } export default BulkActions;
-
Create
BulkActions.cssin thecomponentsfolder:.bulk-actions { background: #f8f9fa; padding: 1rem 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; } .selection-controls { display: flex; align-items: center; gap: 1rem; } .select-all-container { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-weight: 500; } .select-all-checkbox { display: none; } .checkmark { width: 20px; height: 20px; border: 2px solid #6c757d; border-radius: 4px; position: relative; transition: all 0.3s ease; } .select-all-container input:checked + .checkmark { background: #667eea; border-color: #667eea; } .select-all-container input:indeterminate + .checkmark { background: #ffc107; border-color: #ffc107; } .select-all-container input:checked + .checkmark::after { content: '✓'; position: absolute; top: -2px; left: 3px; color: white; font-weight: bold; font-size: 14px; } .select-all-container input:indeterminate + .checkmark::after { content: '−'; position: absolute; top: -2px; left: 4px; color: white; font-weight: bold; font-size: 14px; } .select-text { color: #495057; } .clear-selection { background: #dc3545; color: white; border: none; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: background-color 0.3s ease; } .clear-selection:hover { background: #c82333; } .bulk-action-controls { position: relative; } .bulk-actions-toggle { background: #667eea; color: white; border: none; padding: 0.6rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 500; transition: background-color 0.3s ease; } .bulk-actions-toggle:hover { background: #5a67d8; } .bulk-actions-menu { position: absolute; top: 100%; right: 0; margin-top: 0.5rem; background: white; border: 1px solid #e9ecef; border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); z-index: 100; min-width: 180px; } .bulk-action-btn { width: 100%; padding: 0.8rem 1rem; border: none; background: white; text-align: left; cursor: pointer; transition: background-color 0.3s ease; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; } .bulk-action-btn:hover { background: #f8f9fa; } .bulk-action-btn:first-child { border-radius: 8px 8px 0 0; } .bulk-action-btn:last-child { border-radius: 0 0 8px 8px; } .bulk-action-btn.export { color: #007bff; } .bulk-action-btn.activate { color: #28a745; } .bulk-action-btn.deactivate { color: #dc3545; } @media (max-width: 768px) { .bulk-actions { flex-direction: column; align-items: stretch; } .selection-controls { justify-content: center; } .bulk-actions-menu { position: static; margin-top: 1rem; box-shadow: none; border: 1px solid #e9ecef; } }
-
Update
EmployeeCard.jsto include selection functionality:import React from 'react'; import './EmployeeCard.css'; function EmployeeCard({ employee, isSelected, onSelectionChange }) { const formatSalary = (salary) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(salary); }; const formatHireDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); }; const getStatusClass = (status) => { return status.toLowerCase().replace(' ', '-'); }; const handleCardClick = (e) => { // Don't trigger selection if clicking on interactive elements if (e.target.closest('.employee-checkbox')) { return; } onSelectionChange && onSelectionChange(employee.id); }; return ( <div className={`employee-card ${isSelected ? 'selected' : ''}`} onClick={handleCardClick} > {/* Selection checkbox */} <div className="employee-checkbox" onClick={(e) => e.stopPropagation()}> <input type="checkbox" checked={isSelected || false} onChange={() => onSelectionChange && onSelectionChange(employee.id)} id={`select-${employee.id}`} /> <label htmlFor={`select-${employee.id}`} className="checkbox-label"></label> </div> <div className="employee-header"> <div className="profile-section"> <span className="profile-image">{employee.profileImage}</span> <div className="name-section"> <h3 className="employee-name"> {employee.firstName} {employee.lastName} </h3> <p className="employee-position">{employee.position}</p> </div> </div> <div className={`status-badge ${getStatusClass(employee.status)}`}> {employee.status} </div> </div> <div className="employee-details"> <div className="detail-row"> <span className="detail-label">Department:</span> <span className="detail-value">{employee.department}</span> </div> <div className="detail-row"> <span className="detail-label">Location:</span> <span className="detail-value">{employee.location}</span> </div> <div className="detail-row"> <span className="detail-label">Email:</span> <span className="detail-value">{employee.email}</span> </div> <div className="detail-row"> <span className="detail-label">Phone:</span> <span className="detail-value">{employee.phone}</span> </div> <div className="detail-row"> <span className="detail-label">Hire Date:</span> <span className="detail-value">{formatHireDate(employee.hireDate)}</span> </div> <div className="detail-row"> <span className="detail-label">Salary:</span> <span className="detail-value salary">{formatSalary(employee.salary)}</span> </div> </div> <div className="skills-section"> <span className="skills-label">Skills:</span> <div className="skills-list"> {employee.skills.map((skill, index) => ( <span key={index} className="skill-tag"> {skill} </span> ))} </div> </div> </div> ); } export default EmployeeCard;
-
Update
EmployeeCard.cssto include selection styles:/* Add these styles to the existing EmployeeCard.css */ .employee-card { position: relative; cursor: pointer; user-select: none; } .employee-card.selected { border: 2px solid #667eea; background: #f8f9ff; } .employee-checkbox { position: absolute; top: 1rem; right: 1rem; z-index: 10; } .employee-checkbox input[type="checkbox"] { display: none; } .checkbox-label { display: block; width: 20px; height: 20px; border: 2px solid #6c757d; border-radius: 4px; cursor: pointer; position: relative; transition: all 0.3s ease; background: white; } .employee-checkbox input[type="checkbox"]:checked + .checkbox-label { background: #667eea; border-color: #667eea; } .employee-checkbox input[type="checkbox"]:checked + .checkbox-label::after { content: '✓'; position: absolute; top: -2px; left: 3px; color: white; font-weight: bold; font-size: 14px; } .employee-card:hover .checkbox-label { border-color: #667eea; } /* Ensure header doesn't overlap with checkbox */ .employee-header { margin-right: 2.5rem; }
- Update
src/App.jswith final advanced features:import React, { useState, useMemo } from 'react'; import EmployeeCard from './components/EmployeeCard'; import SearchBar from './components/SearchBar'; import FilterControls from './components/FilterControls'; import SortControls from './components/SortControls'; import BulkActions from './components/BulkActions'; import Pagination from './components/Pagination'; import PerformanceMonitor from './components/PerformanceMonitor'; import { employees, departments, locations } from './data/employeeData'; import './App.css'; function App() { // Existing state const [searchTerm, setSearchTerm] = useState(''); const [selectedDepartment, setSelectedDepartment] = useState('All Departments'); const [selectedLocation, setSelectedLocation] = useState('All Locations'); const [selectedStatus, setSelectedStatus] = useState('All Status'); const [sortBy, setSortBy] = useState('name'); const [sortOrder, setSortOrder] = useState('asc'); // New state for advanced features const [selectedEmployees, setSelectedEmployees] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [renderTime, setRenderTime] = useState(0); // Enhanced filtered and sorted employee list (same as before) const processedEmployees = useMemo(() => { const startTime = performance.now(); let filtered = employees; // Apply search filter if (searchTerm) { const searchLower = searchTerm.toLowerCase(); filtered = filtered.filter(employee => { const fullName = `${employee.firstName} ${employee.lastName}`.toLowerCase(); const position = employee.position.toLowerCase(); const department = employee.department.toLowerCase(); const skills = employee.skills.join(' ').toLowerCase(); const email = employee.email.toLowerCase(); return fullName.includes(searchLower) || position.includes(searchLower) || department.includes(searchLower) || skills.includes(searchLower) || email.includes(searchLower); }); } // Apply filters... if (selectedDepartment !== 'All Departments') { filtered = filtered.filter(employee => employee.department === selectedDepartment); } if (selectedLocation !== 'All Locations') { filtered = filtered.filter(employee => employee.location === selectedLocation); } if (selectedStatus !== 'All Status') { filtered = filtered.filter(employee => employee.status === selectedStatus); } // Apply sorting... const sorted = [...filtered].sort((a, b) => { let aValue, bValue; switch (sortBy) { case 'name': aValue = `${a.firstName} ${a.lastName}`.toLowerCase(); bValue = `${b.firstName} ${b.lastName}`.toLowerCase(); break; case 'position': aValue = a.position.toLowerCase(); bValue = b.position.toLowerCase(); break; case 'department': aValue = a.department.toLowerCase(); bValue = b.department.toLowerCase(); break; case 'location': aValue = a.location.toLowerCase(); bValue = b.location.toLowerCase(); break; case 'hireDate': aValue = new Date(a.hireDate); bValue = new Date(b.hireDate); break; case 'salary': aValue = a.salary; bValue = b.salary; break; default: aValue = a.id; bValue = b.id; } if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1; if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1; return 0; }); const endTime = performance.now(); setRenderTime(endTime - startTime); return sorted; }, [employees, searchTerm, selectedDepartment, selectedLocation, selectedStatus, sortBy, sortOrder]); // Paginated employees const paginatedEmployees = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; return processedEmployees.slice(startIndex, endIndex); }, [processedEmployees, currentPage, itemsPerPage]); // Selection handlers const handleEmployeeSelection = (employeeId) => { setSelectedEmployees(prev => prev.includes(employeeId) ? prev.filter(id => id !== employeeId) : [...prev, employeeId] ); }; const handleSelectAll = () => { setSelectedEmployees(paginatedEmployees.map(emp => emp.id)); }; const handleDeselectAll = () => { setSelectedEmployees([]); }; // Bulk action handlers const handleBulkExport = (selectedIds) => { const selectedData = employees.filter(emp => selectedIds.includes(emp.id)); const csvContent = [ ['Name', 'Position', 'Department', 'Email', 'Phone'].join(','), ...selectedData.map(emp => [`${emp.firstName} ${emp.lastName}`, emp.position, emp.department, emp.email, emp.phone].join(',') ) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'selected_employees.csv'; a.click(); window.URL.revokeObjectURL(url); alert(`Exported ${selectedIds.length} employees to CSV file`); }; const handleBulkStatusChange = (selectedIds, newStatus) => { alert(`In a real app, ${selectedIds.length} employees would be marked as ${newStatus}`); }; // Reset selections when filters change React.useEffect(() => { setSelectedEmployees([]); setCurrentPage(1); }, [searchTerm, selectedDepartment, selectedLocation, selectedStatus]); // Functions const clearAllFilters = () => { setSearchTerm(''); setSelectedDepartment('All Departments'); setSelectedLocation('All Locations'); setSelectedStatus('All Status'); setSortBy('name'); setSortOrder('asc'); setSelectedEmployees([]); setCurrentPage(1); }; const handlePageChange = (page) => { setCurrentPage(page); setSelectedEmployees([]); // Clear selections when changing pages }; const handleItemsPerPageChange = (newItemsPerPage) => { setItemsPerPage(newItemsPerPage); setCurrentPage(1); setSelectedEmployees([]); }; // Calculate statistics const stats = useMemo(() => ({ total: processedEmployees.length, active: processedEmployees.filter(emp => emp.status === 'Active').length, departments: new Set(processedEmployees.map(emp => emp.department)).size }), [processedEmployees]); return ( <div className="App"> <PerformanceMonitor employeeCount={paginatedEmployees.length} renderTime={renderTime} /> <div className="employee-directory"> <header className="directory-header"> <h1>Employee Directory</h1> <p>Find and connect with your colleagues</p> <div className="stats"> <div className="stat-item"> <span className="stat-number">{stats.total}</span> <span className="stat-label"> {searchTerm || selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status' ? 'Filtered Results' : 'Total Employees'} </span> </div> <div className="stat-item"> <span className="stat-number">{stats.active}</span> <span className="stat-label">Active</span> </div> <div className="stat-item"> <span className="stat-number">{stats.departments}</span> <span className="stat-label">Departments</span> </div> </div> </header> <main className="directory-content"> <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} placeholder="Search by name, position, department, skills, or email..." /> <FilterControls departments={departments} locations={locations} selectedDepartment={selectedDepartment} setSelectedDepartment={setSelectedDepartment} selectedLocation={selectedLocation} setSelectedLocation={setSelectedLocation} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} onClearFilters={clearAllFilters} /> <SortControls sortBy={sortBy} setSortBy={setSortBy} sortOrder={sortOrder} setSortOrder={setSortOrder} /> <BulkActions selectedEmployees={selectedEmployees} onSelectAll={handleSelectAll} onDeselectAll={handleDeselectAll} totalEmployees={paginatedEmployees.length} onBulkExport={handleBulkExport} onBulkStatusChange={handleBulkStatusChange} /> {(searchTerm || selectedDepartment !== 'All Departments' || selectedLocation !== 'All Locations' || selectedStatus !== 'All Status') && ( <div className="results-summary"> <p> Showing {paginatedEmployees.length} of {processedEmployees.length} employees {searchTerm && ` matching "${searchTerm}"`} {' '}sorted by {sortBy} ({sortOrder === 'asc' ? 'ascending' : 'descending'}) {processedEmployees.length !== employees.length && ` (filtered from ${employees.length} total)` } </p> </div> )} <div className="employees-grid"> {paginatedEmployees.map(employee => ( <EmployeeCard key={employee.id} employee={employee} isSelected={selectedEmployees.includes(employee.id)} onSelectionChange={handleEmployeeSelection} /> ))} </div> {processedEmployees.length === 0 && ( <div className="no-employees"> <h3>No employees found</h3> <p> {searchTerm ? `No employees match your search for "${searchTerm}".` : 'No employees match your current filters.' } </p> <button className="clear-all-btn" onClick={clearAllFilters}> Clear All Filters & Reset </button> </div> )} <Pagination currentPage={currentPage} totalItems={processedEmployees.length} itemsPerPage={itemsPerPage} onPageChange={handlePageChange} onItemsPerPageChange={handleItemsPerPageChange} /> </main> </div> </div> ); } export default App;
-
Save all files and test the advanced functionality:
Pagination Tests:
- Change items per page (5, 10, 20, 50) and observe page count updates
- Navigate through pages using the pagination controls
- Test pagination with different filter combinations
- Verify page numbers display correctly with ellipsis for many pages
- Test edge cases (single page, no results)
Selection and Bulk Actions Tests:
- Click individual checkboxes to select employees
- Use the "Select all" checkbox to select all visible employees
- Test the indeterminate state (some but not all selected)
- Click "Actions" button to see bulk action menu
- Test bulk export functionality (downloads CSV file)
- Test bulk status change actions (shows alert)
- Verify selection clears when changing pages or filters
Integration Tests:
- Apply filters, then test pagination
- Sort data, then test selections
- Search for employees, then test bulk actions
- Change items per page with selections active
- Test performance monitor with different page sizes
-
Verify the following behaviors:
- Selections are cleared when filters or pages change
- Pagination correctly shows filtered results count
- Bulk actions work only with selected items
- Export functionality generates a proper CSV file
- Performance metrics update with page size changes
- All previous functionality (search, filter, sort) still works
-
Test production scenarios:
- Select employees across multiple criteria
- Test with maximum items per page (50)
- Try rapid filter changes and observe performance
- Test bulk export with various selection combinations
Exercise 4 Summary: You have successfully implemented advanced list management features including pagination, bulk actions, and performance monitoring. You learned how to implement efficient pagination with smart page number display, create bulk selection and action systems, handle CSV export functionality, manage complex state interactions between features, optimize performance for large datasets, and provide comprehensive user feedback. This completes a production-ready employee directory with all the features users would expect in a modern web application.
Congratulations! You have successfully completed the Employee Directory lab for Lesson 5. Throughout this comprehensive lab, you accomplished:
✅ Mastered List Rendering: Implemented efficient rendering of dynamic employee data using map()
✅ Applied React Keys: Used proper key props for optimal list performance and reconciliation
✅ Built Search Functionality: Created real-time search across multiple employee fields
✅ Implemented Filtering: Added dropdown filters for department, location, and status
✅ Created Sorting System: Built interactive sorting by multiple criteria with visual feedback
✅ Added Pagination: Implemented smart pagination with configurable page sizes
✅ Built Bulk Actions: Created selection and bulk operation functionality
✅ Optimized Performance: Used useMemo, proper keys, and performance monitoring
List Rendering Fundamentals:
- Array.map(): Rendering arrays of data into React components
- React Keys: Proper key usage for list item identification and performance
- Component Composition: Building complex UIs from reusable components
- Conditional Rendering: Displaying different content based on data states
Dynamic Data Manipulation:
- Array.filter(): Filtering data based on multiple criteria
- Array.sort(): Sorting data by different field types (strings, numbers, dates)
- Search Implementation: Real-time search across multiple fields
- State-Driven UI: UI that responds to data changes immediately
Performance Optimization:
- useMemo: Preventing unnecessary recalculations of expensive operations
- Proper React Keys: Optimizing reconciliation and re-renders
- Pagination: Handling large datasets efficiently
- Performance Monitoring: Measuring and tracking render performance
Advanced Features:
- Bulk Operations: Multi-selection and batch actions
- Export Functionality: CSV file generation and download
- Complex State Management: Coordinating multiple related state variables
- User Experience: Comprehensive feedback, loading states, and error handling
- Enterprise UI Patterns: Features common in business applications
- Data Management: Handling, filtering, and presenting large datasets
- User Experience Design: Intuitive interfaces with clear feedback
- Performance Optimization: Building responsive applications that scale
- File Operations: Generating and downloading data exports
- Complex State Coordination: Managing interdependent features and state
- Real-time search with instant results
- Multi-criteria filtering with active filter indicators
- Flexible sorting with visual direction indicators
- Smart pagination with ellipsis and page size options
- Bulk selection with select-all and indeterminate states
- CSV export functionality for data portability
- Performance monitoring for optimization insights
- Responsive design for mobile and desktop users
Your employee directory demonstrates mastery of list rendering, keys, and dynamic data manipulation in React. In future lessons, you'll build on these fundamentals to learn about forms, component patterns, and advanced React concepts. The skills you've gained here are directly applicable to building data-driven applications in professional development environments.
Time Completed: ~30 minutes total
- Always use proper React keys for list items to ensure optimal performance
- Implement useMemo for expensive calculations to prevent unnecessary re-renders
- Provide comprehensive user feedback for all interactions and states
- Design for scalability with pagination and virtual scrolling for large datasets
- Consider user experience with bulk actions, clear filters, and intuitive navigation
- Monitor performance and optimize bottlenecks in data processing
- Structure components for reusability and maintainable code organization