Skip to content

Latest commit

 

History

History
1491 lines (1275 loc) · 41.2 KB

File metadata and controls

1491 lines (1275 loc) · 41.2 KB

React Fundamentals Course

Lesson 3 Lab: Interactive Task Manager

Lab Overview

In this hands-on lab, you will build a comprehensive task management application that demonstrates state management using React's useState hook. You'll learn how to handle form inputs, manage component state, implement controlled components, and handle various user interactions. The application will allow users to add, edit, complete, and delete tasks with proper form validation and different task states.

Total Estimated Time: 45 minutes


Exercise 1: Project Setup and Initial State Structure

Estimated Time: 8 minutes

In this exercise, you will create a new React project for the task manager and set up the initial state structure using the useState hook. You'll learn how to initialize state with complex data structures and understand the fundamentals of state management in React.

Step 1.1: Create a New React Project

  1. Open VS Code on your Windows virtual machine
  2. Open the integrated terminal (`Ctrl + ``)
  3. Navigate to your desired project directory:
    cd Desktop
  4. Create a new React application called "task-manager":
    npx create-react-app task-manager
  5. Navigate into the project directory:
    cd task-manager
  6. Open the project in VS Code (File > Open Folder and select task-manager)

Step 1.2: Start the Development Server

  1. Start the development server:
    npm start
  2. Verify the application loads in your browser at http://localhost:3000

Step 1.3: Create Initial App Structure with State

  1. Replace the contents of src/App.js with:
    import React, { useState } from 'react';
    import './App.css';
    
    function App() {
      // Initialize state for tasks - each task has an id, text, completed status, priority, and dueDate
      const [tasks, setTasks] = useState([
        {
          id: 1,
          text: "Complete React fundamentals course",
          completed: false,
          priority: "high",
          dueDate: "2024-12-25",
          createdAt: new Date().toISOString()
        },
        {
          id: 2,
          text: "Build portfolio website",
          completed: true,
          priority: "medium",
          dueDate: "2024-12-20",
          createdAt: new Date().toISOString()
        },
        {
          id: 3,
          text: "Practice JavaScript algorithms",
          completed: false,
          priority: "low",
          dueDate: "2024-12-30",
          createdAt: new Date().toISOString()
        }
      ]);
    
      // State for the new task form
      const [newTask, setNewTask] = useState({
        text: '',
        priority: 'medium',
        dueDate: ''
      });
    
      // State for form validation errors
      const [errors, setErrors] = useState({});
    
      // State for editing a task
      const [editingId, setEditingId] = useState(null);
      const [editText, setEditText] = useState('');
    
      // State for filtering tasks
      const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
    
      return (
        <div className="App">
          <div className="task-manager">
            <h1>Task Manager</h1>
            <p>Manage your daily tasks efficiently</p>
            
            {/* We'll add components here in the next exercises */}
            <div className="task-stats">
              <p>Total Tasks: {tasks.length}</p>
              <p>Completed: {tasks.filter(task => task.completed).length}</p>
              <p>Pending: {tasks.filter(task => !task.completed).length}</p>
            </div>
          </div>
        </div>
      );
    }
    
    export default App;
    This establishes multiple state variables using useState hook, demonstrating different types of state: arrays, objects, strings, and null values.

Step 1.4: Add Basic Styling

  1. Replace the contents of src/App.css with:

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f5f7fa;
      line-height: 1.6;
    }
    
    .App {
      min-height: 100vh;
      padding: 2rem;
    }
    
    .task-manager {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      border-radius: 12px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }
    
    .task-manager h1 {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 2rem;
      margin: 0;
      text-align: center;
      font-size: 2.5rem;
    }
    
    .task-manager > p {
      text-align: center;
      color: #666;
      padding: 1rem 2rem 0;
    }
    
    .task-stats {
      display: flex;
      justify-content: space-around;
      padding: 1rem 2rem;
      background: #f8f9fa;
      border-bottom: 1px solid #e9ecef;
    }
    
    .task-stats p {
      color: #495057;
      font-weight: 600;
    }
  2. Save the files and check your browser to see the initial layout with task statistics

Exercise 1 Summary: You have successfully set up a React project with multiple useState hooks to manage different aspects of application state. You learned how to initialize state with complex data structures including arrays of objects, form state, error state, and filter state. The foundation is now ready for building interactive features.


Exercise 2: Create Task Form with Controlled Components

Estimated Time: 12 minutes

In this exercise, you will create a form component that allows users to add new tasks. You'll learn about controlled components, form validation, and how to update state based on user input. This demonstrates the core concept of binding form inputs to state variables.

Step 2.1: Create the TaskForm Component

  1. Create a components folder in the src directory
  2. Create a new file called TaskForm.js in the components folder
  3. Add the following code:
    import React from 'react';
    import './TaskForm.css';
    
    function TaskForm({ 
      newTask, 
      setNewTask, 
      onSubmit, 
      errors 
    }) {
      // Handle input changes - this updates state as user types
      const handleInputChange = (e) => {
        const { name, value } = e.target;
        setNewTask(prev => ({
          ...prev,
          [name]: value
        }));
      };
    
      // Handle form submission
      const handleSubmit = (e) => {
        e.preventDefault(); // Prevent page refresh
        onSubmit();
      };
    
      // Get today's date for minimum date validation
      const today = new Date().toISOString().split('T')[0];
    
      return (
        <form className="task-form" onSubmit={handleSubmit}>
          <div className="form-header">
            <h3>Add New Task</h3>
          </div>
    
          <div className="form-group">
            <label htmlFor="task-text">Task Description *</label>
            <input
              type="text"
              id="task-text"
              name="text"
              value={newTask.text}
              onChange={handleInputChange}
              placeholder="Enter task description..."
              className={errors.text ? 'error' : ''}
            />
            {errors.text && <span className="error-message">{errors.text}</span>}
          </div>
    
          <div className="form-row">
            <div className="form-group">
              <label htmlFor="task-priority">Priority</label>
              <select
                id="task-priority"
                name="priority"
                value={newTask.priority}
                onChange={handleInputChange}
              >
                <option value="low">Low</option>
                <option value="medium">Medium</option>
                <option value="high">High</option>
              </select>
            </div>
    
            <div className="form-group">
              <label htmlFor="task-due-date">Due Date</label>
              <input
                type="date"
                id="task-due-date"
                name="dueDate"
                value={newTask.dueDate}
                onChange={handleInputChange}
                min={today}
                className={errors.dueDate ? 'error' : ''}
              />
              {errors.dueDate && <span className="error-message">{errors.dueDate}</span>}
            </div>
          </div>
    
          <button type="submit" className="submit-btn">
            Add Task
          </button>
        </form>
      );
    }
    
    export default TaskForm;
    This demonstrates controlled components where form inputs are bound to state and updated through onChange handlers.

Step 2.2: Create TaskForm Styles

  1. Create TaskForm.css in the components folder:
    .task-form {
      padding: 2rem;
      border-bottom: 1px solid #e9ecef;
      background: #f8f9fa;
    }
    
    .form-header h3 {
      color: #495057;
      margin-bottom: 1.5rem;
      font-size: 1.3rem;
    }
    
    .form-group {
      margin-bottom: 1.5rem;
    }
    
    .form-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
    }
    
    .form-group label {
      display: block;
      margin-bottom: 0.5rem;
      color: #495057;
      font-weight: 600;
    }
    
    .form-group input,
    .form-group select {
      width: 100%;
      padding: 0.8rem;
      border: 2px solid #e9ecef;
      border-radius: 6px;
      font-size: 1rem;
      transition: border-color 0.3s ease;
    }
    
    .form-group input:focus,
    .form-group select:focus {
      outline: none;
      border-color: #667eea;
    }
    
    .form-group input.error,
    .form-group select.error {
      border-color: #e74c3c;
    }
    
    .error-message {
      color: #e74c3c;
      font-size: 0.9rem;
      margin-top: 0.3rem;
      display: block;
    }
    
    .submit-btn {
      background: #667eea;
      color: white;
      border: none;
      padding: 0.8rem 2rem;
      border-radius: 6px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }
    
    .submit-btn:hover {
      background: #5a6fd8;
    }
    
    @media (max-width: 768px) {
      .form-row {
        grid-template-columns: 1fr;
      }
    }

Step 2.3: Add Form Logic to App Component

  1. Update src/App.js to include the form handling logic:

    import React, { useState } from 'react';
    import TaskForm from './components/TaskForm';
    import './App.css';
    
    function App() {
      // ... (keep existing state from previous exercise)
      const [tasks, setTasks] = useState([
        {
          id: 1,
          text: "Complete React fundamentals course",
          completed: false,
          priority: "high",
          dueDate: "2024-12-25",
          createdAt: new Date().toISOString()
        },
        {
          id: 2,
          text: "Build portfolio website",
          completed: true,
          priority: "medium",
          dueDate: "2024-12-20",
          createdAt: new Date().toISOString()
        },
        {
          id: 3,
          text: "Practice JavaScript algorithms",
          completed: false,
          priority: "low",
          dueDate: "2024-12-30",
          createdAt: new Date().toISOString()
        }
      ]);
    
      const [newTask, setNewTask] = useState({
        text: '',
        priority: 'medium',
        dueDate: ''
      });
    
      const [errors, setErrors] = useState({});
      const [editingId, setEditingId] = useState(null);
      const [editText, setEditText] = useState('');
      const [filter, setFilter] = useState('all');
    
      // Form validation function
      const validateForm = () => {
        const newErrors = {};
    
        if (!newTask.text.trim()) {
          newErrors.text = 'Task description is required';
        } else if (newTask.text.trim().length < 3) {
          newErrors.text = 'Task description must be at least 3 characters';
        }
    
        if (!newTask.dueDate) {
          newErrors.dueDate = 'Due date is required';
        } else {
          const selectedDate = new Date(newTask.dueDate);
          const today = new Date();
          today.setHours(0, 0, 0, 0);
          
          if (selectedDate < today) {
            newErrors.dueDate = 'Due date cannot be in the past';
          }
        }
    
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
      };
    
      // Handle adding a new task
      const handleAddTask = () => {
        if (validateForm()) {
          const task = {
            id: Date.now(), // Simple ID generation
            text: newTask.text.trim(),
            completed: false,
            priority: newTask.priority,
            dueDate: newTask.dueDate,
            createdAt: new Date().toISOString()
          };
    
          setTasks(prevTasks => [...prevTasks, task]);
          
          // Reset form
          setNewTask({
            text: '',
            priority: 'medium',
            dueDate: ''
          });
          
          // Clear errors
          setErrors({});
        }
      };
    
      return (
        <div className="App">
          <div className="task-manager">
            <h1>Task Manager</h1>
            <p>Manage your daily tasks efficiently</p>
            
            <TaskForm 
              newTask={newTask}
              setNewTask={setNewTask}
              onSubmit={handleAddTask}
              errors={errors}
            />
            
            <div className="task-stats">
              <p>Total Tasks: {tasks.length}</p>
              <p>Completed: {tasks.filter(task => task.completed).length}</p>
              <p>Pending: {tasks.filter(task => !task.completed).length}</p>
            </div>
          </div>
        </div>
      );
    }
    
    export default App;
  2. Save all files and test the form - try adding tasks with validation

Exercise 2 Summary: You have created a fully functional form with controlled components, form validation, and state updates. You learned how to handle input changes with the spread operator to update specific properties in state objects, implement form validation, and manage form submission. The form demonstrates proper controlled component patterns where React state is the single source of truth.


Exercise 3: Create Task List with State Manipulation

Estimated Time: 12 minutes

In this exercise, you will create a TaskList component that displays tasks and allows users to mark them as complete, delete them, and edit them inline. You'll learn about different patterns for updating state, handling events, and implementing conditional rendering based on state.

Step 3.1: Create the TaskItem Component

  1. Create TaskItem.js in the components folder:
    import React, { useState } from 'react';
    import './TaskItem.css';
    
    function TaskItem({ 
      task, 
      onToggleComplete, 
      onDelete, 
      onEdit,
      isEditing,
      onStartEdit,
      onCancelEdit
    }) {
      const [editText, setEditText] = useState(task.text);
    
      const handleEditSubmit = (e) => {
        e.preventDefault();
        if (editText.trim() && editText.trim() !== task.text) {
          onEdit(task.id, editText.trim());
        } else {
          onCancelEdit();
        }
      };
    
      const getPriorityClass = (priority) => {
        return `priority-${priority}`;
      };
    
      const isOverdue = () => {
        if (task.completed) return false;
        const today = new Date();
        const dueDate = new Date(task.dueDate);
        return dueDate < today;
      };
    
      const formatDate = (dateString) => {
        const date = new Date(dateString);
        return date.toLocaleDateString('en-US', {
          month: 'short',
          day: 'numeric',
          year: 'numeric'
        });
      };
    
      if (isEditing) {
        return (
          <div className="task-item editing">
            <form onSubmit={handleEditSubmit} className="edit-form">
              <input
                type="text"
                value={editText}
                onChange={(e) => setEditText(e.target.value)}
                className="edit-input"
                autoFocus
              />
              <div className="edit-actions">
                <button type="submit" className="save-btn">Save</button>
                <button type="button" onClick={onCancelEdit} className="cancel-btn">
                  Cancel
                </button>
              </div>
            </form>
          </div>
        );
      }
    
      return (
        <div className={`task-item ${task.completed ? 'completed' : ''} ${isOverdue() ? 'overdue' : ''}`}>
          <div className="task-content">
            <div className="task-checkbox">
              <input
                type="checkbox"
                checked={task.completed}
                onChange={() => onToggleComplete(task.id)}
                id={`task-${task.id}`}
              />
              <label htmlFor={`task-${task.id}`} className="checkbox-label"></label>
            </div>
    
            <div className="task-details">
              <div className="task-text">{task.text}</div>
              <div className="task-meta">
                <span className={`priority-badge ${getPriorityClass(task.priority)}`}>
                  {task.priority.toUpperCase()}
                </span>
                <span className="due-date">
                  Due: {formatDate(task.dueDate)}
                  {isOverdue() && <span className="overdue-indicator"> (OVERDUE)</span>}
                </span>
              </div>
            </div>
          </div>
    
          <div className="task-actions">
            <button 
              onClick={() => onStartEdit(task.id)} 
              className="edit-btn"
              disabled={task.completed}
            >
              Edit
            </button>
            <button 
              onClick={() => onDelete(task.id)} 
              className="delete-btn"
            >
              Delete
            </button>
          </div>
        </div>
      );
    }
    
    export default TaskItem;

Step 3.2: Create TaskItem Styles

  1. Create TaskItem.css:
    .task-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem;
      border-bottom: 1px solid #e9ecef;
      transition: background-color 0.3s ease;
    }
    
    .task-item:hover {
      background-color: #f8f9fa;
    }
    
    .task-item.completed {
      opacity: 0.7;
      background-color: #f1f8ff;
    }
    
    .task-item.overdue {
      border-left: 4px solid #e74c3c;
      background-color: #fef2f2;
    }
    
    .task-item.editing {
      background-color: #fff3cd;
    }
    
    .task-content {
      display: flex;
      align-items: center;
      flex-grow: 1;
      gap: 1rem;
    }
    
    .task-checkbox {
      position: relative;
    }
    
    .task-checkbox input[type="checkbox"] {
      opacity: 0;
      position: absolute;
    }
    
    .checkbox-label {
      display: block;
      width: 20px;
      height: 20px;
      border: 2px solid #6c757d;
      border-radius: 4px;
      cursor: pointer;
      position: relative;
      transition: all 0.3s ease;
    }
    
    .task-checkbox input[type="checkbox"]:checked + .checkbox-label {
      background-color: #28a745;
      border-color: #28a745;
    }
    
    .task-checkbox input[type="checkbox"]:checked + .checkbox-label::after {
      content: '✓';
      position: absolute;
      top: -2px;
      left: 3px;
      color: white;
      font-weight: bold;
      font-size: 14px;
    }
    
    .task-details {
      flex-grow: 1;
    }
    
    .task-text {
      font-size: 1.1rem;
      color: #495057;
      margin-bottom: 0.5rem;
    }
    
    .task-item.completed .task-text {
      text-decoration: line-through;
      color: #6c757d;
    }
    
    .task-meta {
      display: flex;
      align-items: center;
      gap: 1rem;
      font-size: 0.9rem;
    }
    
    .priority-badge {
      padding: 0.2rem 0.6rem;
      border-radius: 12px;
      font-size: 0.8rem;
      font-weight: bold;
    }
    
    .priority-high {
      background-color: #dc3545;
      color: white;
    }
    
    .priority-medium {
      background-color: #ffc107;
      color: #000;
    }
    
    .priority-low {
      background-color: #28a745;
      color: white;
    }
    
    .due-date {
      color: #6c757d;
    }
    
    .overdue-indicator {
      color: #e74c3c;
      font-weight: bold;
    }
    
    .task-actions {
      display: flex;
      gap: 0.5rem;
    }
    
    .edit-btn, .delete-btn, .save-btn, .cancel-btn {
      padding: 0.4rem 0.8rem;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 0.9rem;
      transition: background-color 0.3s ease;
    }
    
    .edit-btn {
      background-color: #007bff;
      color: white;
    }
    
    .edit-btn:hover:not(:disabled) {
      background-color: #0056b3;
    }
    
    .edit-btn:disabled {
      background-color: #6c757d;
      cursor: not-allowed;
    }
    
    .delete-btn {
      background-color: #dc3545;
      color: white;
    }
    
    .delete-btn:hover {
      background-color: #c82333;
    }
    
    .edit-form {
      display: flex;
      align-items: center;
      gap: 1rem;
      width: 100%;
    }
    
    .edit-input {
      flex-grow: 1;
      padding: 0.5rem;
      border: 2px solid #007bff;
      border-radius: 4px;
      font-size: 1rem;
    }
    
    .edit-actions {
      display: flex;
      gap: 0.5rem;
    }
    
    .save-btn {
      background-color: #28a745;
      color: white;
    }
    
    .cancel-btn {
      background-color: #6c757d;
      color: white;
    }

Step 3.3: Create TaskList Component

  1. Create TaskList.js in the components folder:
    import React from 'react';
    import TaskItem from './TaskItem';
    import './TaskList.css';
    
    function TaskList({ 
      tasks, 
      onToggleComplete, 
      onDelete, 
      onEdit,
      editingId,
      onStartEdit,
      onCancelEdit,
      filter 
    }) {
      // Filter tasks based on current filter
      const filteredTasks = tasks.filter(task => {
        switch (filter) {
          case 'active':
            return !task.completed;
          case 'completed':
            return task.completed;
          default:
            return true;
        }
      });
    
      // Sort tasks: incomplete first, then by priority, then by due date
      const sortedTasks = [...filteredTasks].sort((a, b) => {
        // First, sort by completion status
        if (a.completed !== b.completed) {
          return a.completed ? 1 : -1;
        }
    
        // Then by priority
        const priorityOrder = { high: 3, medium: 2, low: 1 };
        if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
          return priorityOrder[b.priority] - priorityOrder[a.priority];
        }
    
        // Finally by due date
        return new Date(a.dueDate) - new Date(b.dueDate);
      });
    
      if (sortedTasks.length === 0) {
        return (
          <div className="task-list">
            <div className="empty-state">
              <h3>No tasks found</h3>
              <p>
                {filter === 'all' 
                  ? 'Add a task to get started!' 
                  : `No ${filter} tasks found.`
                }
              </p>
            </div>
          </div>
        );
      }
    
      return (
        <div className="task-list">
          <div className="task-list-header">
            <h3>
              {filter === 'all' && 'All Tasks'}
              {filter === 'active' && 'Active Tasks'}
              {filter === 'completed' && 'Completed Tasks'}
              <span className="task-count">({sortedTasks.length})</span>
            </h3>
          </div>
          
          {sortedTasks.map(task => (
            <TaskItem
              key={task.id}
              task={task}
              onToggleComplete={onToggleComplete}
              onDelete={onDelete}
              onEdit={onEdit}
              isEditing={editingId === task.id}
              onStartEdit={onStartEdit}
              onCancelEdit={onCancelEdit}
            />
          ))}
        </div>
      );
    }
    
    export default TaskList;

Step 3.4: Create TaskList Styles

  1. Create TaskList.css:
    .task-list {
      min-height: 200px;
    }
    
    .task-list-header {
      padding: 1rem 2rem;
      background-color: #f8f9fa;
      border-bottom: 2px solid #e9ecef;
    }
    
    .task-list-header h3 {
      color: #495057;
      margin: 0;
      display: flex;
      align-items: center;
      gap: 0.5rem;
    }
    
    .task-count {
      color: #6c757d;
      font-weight: normal;
      font-size: 0.9rem;
    }
    
    .empty-state {
      text-align: center;
      padding: 3rem 2rem;
      color: #6c757d;
    }
    
    .empty-state h3 {
      margin-bottom: 0.5rem;
      color: #495057;
    }

Exercise 3 Summary: You have created a comprehensive task list with individual task items that support multiple state operations. You learned how to update array state immutably, handle complex state changes like toggling completion status, implement inline editing with local component state, and create conditional rendering based on different states. The components demonstrate proper separation of concerns and state management patterns.


Exercise 4: Add Task Filtering and Complete State Management

Estimated Time: 13 minutes

In this exercise, you will implement task filtering functionality and complete the state management by adding all the task manipulation functions to the main App component. You'll learn about complex state updates, array manipulation methods, and how to coordinate state between multiple components.

Step 4.1: Create TaskFilter Component

  1. Create TaskFilter.js in the components folder:
    import React from 'react';
    import './TaskFilter.css';
    
    function TaskFilter({ 
      filter, 
      onFilterChange, 
      taskCounts 
    }) {
      const filters = [
        { key: 'all', label: 'All Tasks', count: taskCounts.all },
        { key: 'active', label: 'Active', count: taskCounts.active },
        { key: 'completed', label: 'Completed', count: taskCounts.completed }
      ];
    
      return (
        <div className="task-filter">
          <div className="filter-buttons">
            {filters.map(filterOption => (
              <button
                key={filterOption.key}
                className={`filter-btn ${filter === filterOption.key ? 'active' : ''}`}
                onClick={() => onFilterChange(filterOption.key)}
              >
                {filterOption.label}
                <span className="filter-count">({filterOption.count})</span>
              </button>
            ))}
          </div>
    
          <div className="bulk-actions">
            <button 
              className="clear-completed-btn"
              onClick={() => onFilterChange('clear-completed')}
              disabled={taskCounts.completed === 0}
            >
              Clear Completed ({taskCounts.completed})
            </button>
          </div>
        </div>
      );
    }
    
    export default TaskFilter;

Step 4.2: Create TaskFilter Styles

  1. Create TaskFilter.css:
    .task-filter {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem 2rem;
      background-color: white;
      border-bottom: 1px solid #e9ecef;
    }
    
    .filter-buttons {
      display: flex;
      gap: 0.5rem;
    }
    
    .filter-btn {
      background: transparent;
      border: 2px solid #e9ecef;
      color: #495057;
      padding: 0.5rem 1rem;
      border-radius: 20px;
      cursor: pointer;
      transition: all 0.3s ease;
      font-size: 0.9rem;
      display: flex;
      align-items: center;
      gap: 0.3rem;
    }
    
    .filter-btn:hover {
      border-color: #667eea;
      background-color: #f8f9fa;
    }
    
    .filter-btn.active {
      background-color: #667eea;
      border-color: #667eea;
      color: white;
    }
    
    .filter-count {
      background: rgba(0, 0, 0, 0.1);
      padding: 0.1rem 0.4rem;
      border-radius: 10px;
      font-size: 0.8rem;
    }
    
    .filter-btn.active .filter-count {
      background: rgba(255, 255, 255, 0.2);
    }
    
    .clear-completed-btn {
      background: #dc3545;
      color: white;
      border: none;
      padding: 0.5rem 1rem;
      border-radius: 4px;
      cursor: pointer;
      font-size: 0.9rem;
      transition: background-color 0.3s ease;
    }
    
    .clear-completed-btn:hover:not(:disabled) {
      background: #c82333;
    }
    
    .clear-completed-btn:disabled {
      background: #6c757d;
      cursor: not-allowed;
    }
    
    @media (max-width: 768px) {
      .task-filter {
        flex-direction: column;
        gap: 1rem;
        align-items: stretch;
      }
    
      .filter-buttons {
        justify-content: center;
      }
    }

Step 4.3: Complete App Component with All State Management

  1. Update src/App.js with the complete functionality:
    import React, { useState, useEffect } from 'react';
    import TaskForm from './components/TaskForm';
    import TaskList from './components/TaskList';
    import TaskFilter from './components/TaskFilter';
    import './App.css';
    
    function App() {
      // Main tasks state
      const [tasks, setTasks] = useState([
        {
          id: 1,
          text: "Complete React fundamentals course",
          completed: false,
          priority: "high",
          dueDate: "2024-12-25",
          createdAt: new Date().toISOString()
        },
        {
          id: 2,
          text: "Build portfolio website",
          completed: true,
          priority: "medium",
          dueDate: "2024-12-20",
          createdAt: new Date().toISOString()
        },
        {
          id: 3,
          text: "Practice JavaScript algorithms",
          completed: false,
          priority: "low",
          dueDate: "2024-12-30",
          createdAt: new Date().toISOString()
        }
      ]);
    
      // Form state
      const [newTask, setNewTask] = useState({
        text: '',
        priority: 'medium',
        dueDate: ''
      });
    
      // Validation errors state
      const [errors, setErrors] = useState({});
    
      // Editing state
      const [editingId, setEditingId] = useState(null);
    
      // Filter state
      const [filter, setFilter] = useState('all');
    
      // Calculate task counts for filtering
      const taskCounts = {
        all: tasks.length,
        active: tasks.filter(task => !task.completed).length,
        completed: tasks.filter(task => task.completed).length
      };
    
      // Form validation
      const validateForm = () => {
        const newErrors = {};
    
        if (!newTask.text.trim()) {
          newErrors.text = 'Task description is required';
        } else if (newTask.text.trim().length < 3) {
          newErrors.text = 'Task description must be at least 3 characters';
        }
    
        if (!newTask.dueDate) {
          newErrors.dueDate = 'Due date is required';
        } else {
          const selectedDate = new Date(newTask.dueDate);
          const today = new Date();
          today.setHours(0, 0, 0, 0);
          
          if (selectedDate < today) {
            newErrors.dueDate = 'Due date cannot be in the past';
          }
        }
    
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
      };
    
      // Add new task
      const handleAddTask = () => {
        if (validateForm()) {
          const task = {
            id: Date.now(),
            text: newTask.text.trim(),
            completed: false,
            priority: newTask.priority,
            dueDate: newTask.dueDate,
            createdAt: new Date().toISOString()
          };
    
          setTasks(prevTasks => [...prevTasks, task]);
          
          // Reset form
          setNewTask({
            text: '',
            priority: 'medium',
            dueDate: ''
          });
          
          setErrors({});
        }
      };
    
      // Toggle task completion
      const handleToggleComplete = (taskId) => {
        setTasks(prevTasks =>
          prevTasks.map(task =>
            task.id === taskId
              ? { ...task, completed: !task.completed }
              : task
          )
        );
      };
    
      // Delete task
      const handleDeleteTask = (taskId) => {
        setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId));
        // If we're editing the task being deleted, cancel editing
        if (editingId === taskId) {
          setEditingId(null);
        }
      };
    
      // Start editing a task
      const handleStartEdit = (taskId) => {
        setEditingId(taskId);
      };
    
      // Cancel editing
      const handleCancelEdit = () => {
        setEditingId(null);
      };
    
      // Save edited task
      const handleEditTask = (taskId, newText) => {
        setTasks(prevTasks =>
          prevTasks.map(task =>
            task.id === taskId
              ? { ...task, text: newText }
              : task
          )
        );
        setEditingId(null);
      };
    
      // Handle filter changes
      const handleFilterChange = (newFilter) => {
        if (newFilter === 'clear-completed') {
          setTasks(prevTasks => prevTasks.filter(task => !task.completed));
        } else {
          setFilter(newFilter);
        }
      };
    
      // Load tasks from localStorage on component mount
      useEffect(() => {
        const savedTasks = localStorage.getItem('tasks');
        if (savedTasks) {
          try {
            setTasks(JSON.parse(savedTasks));
          } catch (error) {
            console.error('Error loading tasks from localStorage:', error);
          }
        }
      }, []);
    
      // Save tasks to localStorage whenever tasks change
      useEffect(() => {
        localStorage.setItem('tasks', JSON.stringify(tasks));
      }, [tasks]);
    
      return (
        <div className="App">
          <div className="task-manager">
            <h1>Task Manager</h1>
            <p>Manage your daily tasks efficiently</p>
            
            <TaskForm 
              newTask={newTask}
              setNewTask={setNewTask}
              onSubmit={handleAddTask}
              errors={errors}
            />
    
            <TaskFilter
              filter={filter}
              onFilterChange={handleFilterChange}
              taskCounts={taskCounts}
            />
            
            <TaskList
              tasks={tasks}
              onToggleComplete={handleToggleComplete}
              onDelete={handleDeleteTask}
              onEdit={handleEditTask}
              editingId={editingId}
              onStartEdit={handleStartEdit}
              onCancelEdit={handleCancelEdit}
              filter={filter}
            />
            
            <div className="task-stats">
              <div className="stat-item">
                <span className="stat-number">{taskCounts.all}</span>
                <span className="stat-label">Total Tasks</span>
              </div>
              <div className="stat-item">
                <span className="stat-number">{taskCounts.completed}</span>
                <span className="stat-label">Completed</span>
              </div>
              <div className="stat-item">
                <span className="stat-number">{taskCounts.active}</span>
                <span className="stat-label">Active</span>
              </div>
            </div>
          </div>
        </div>
      );
    }
    
    export default App;

Step 4.4: Update App Styles for Better Layout

  1. Update src/App.css:
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f5f7fa;
      line-height: 1.6;
    }
    
    .App {
      min-height: 100vh;
      padding: 2rem;
    }
    
    .task-manager {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      border-radius: 12px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }
    
    .task-manager h1 {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 2rem;
      margin: 0;
      text-align: center;
      font-size: 2.5rem;
    }
    
    .task-manager > p {
      text-align: center;
      color: #666;
      padding: 1rem 2rem 0;
    }
    
    .task-stats {
      display: flex;
      justify-content: space-around;
      padding: 1.5rem 2rem;
      background: #f8f9fa;
      border-top: 1px solid #e9ecef;
    }
    
    .stat-item {
      text-align: center;
      display: flex;
      flex-direction: column;
      gap: 0.3rem;
    }
    
    .stat-number {
      font-size: 1.8rem;
      font-weight: bold;
      color: #667eea;
    }
    
    .stat-label {
      color: #6c757d;
      font-size: 0.9rem;
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }
    
    @media (max-width: 768px) {
      .App {
        padding: 1rem;
      }
    
      .task-manager h1 {
        font-size: 2rem;
        padding: 1.5rem;
      }
    
      .task-stats {
        padding: 1rem;
      }
    
      .stat-number {
        font-size: 1.5rem;
      }
    }

Step 4.5: Test the Complete Application

  1. Save all files and test the complete task manager:
    • Add new tasks with validation
    • Mark tasks as complete/incomplete
    • Edit task descriptions inline
    • Delete tasks
    • Filter tasks by status
    • Clear completed tasks
    • Notice that tasks persist when you refresh the page

Exercise 4 Summary: You have completed a full-featured task management application with comprehensive state management. You learned how to handle complex state updates using the spread operator and array methods, implement localStorage for data persistence using useEffect, coordinate state between multiple components, and manage different types of user interactions including CRUD operations (Create, Read, Update, Delete).


Lab Summary

Congratulations! You have successfully completed the Interactive Task Manager lab. In this comprehensive lab, you accomplished:

Mastered useState Hook: Implemented state management for forms, lists, editing, and filtering
Created Controlled Components: Built form inputs that are fully controlled by React state
Implemented Form Validation: Added real-time validation with error handling and user feedback
Handled Complex State Updates: Used spread operator and array methods for immutable updates
Built Interactive User Interface: Created edit-in-place functionality and dynamic filtering
Added Data Persistence: Used useEffect and localStorage to persist data across sessions
Implemented CRUD Operations: Full Create, Read, Update, Delete functionality for tasks

Key React Concepts Learned:

State Management Fundamentals:

  • useState Hook: Managing different types of state (strings, objects, arrays, booleans)
  • State Updates: Immutable updates using spread operator and array methods
  • Complex State: Managing related pieces of state that work together

Controlled Components:

  • Form Binding: Connecting form inputs to state variables
  • Event Handling: onChange and onSubmit event handlers
  • Two-Way Data Binding: State drives UI, UI updates state

Component Communication:

  • Props vs State: Understanding when to use each
  • Callback Props: Passing functions to child components for state updates
  • State Lifting: Managing shared state in parent components

Side Effects:

  • useEffect Hook: Running code in response to state changes
  • Data Persistence: Saving and loading data from localStorage
  • Component Lifecycle: Understanding when effects run

Advanced Patterns Demonstrated:

State Update Patterns:

  • Array manipulation (add, remove, update items)
  • Object property updates using spread operator
  • Conditional state updates based on user actions
  • Bulk operations (clear completed tasks)

Form Handling:

  • Multi-field form state management
  • Real-time validation and error display
  • Form reset after successful submission
  • Date validation and constraints

User Experience Features:

  • Inline editing with save/cancel functionality
  • Visual feedback for task states (completed, overdue)
  • Dynamic filtering and counting
  • Responsive design for mobile devices

Real-World Skills Acquired:

  • Task Management Systems: Understanding common patterns in productivity apps
  • Form Validation: Implementing robust client-side validation
  • Data Persistence: Working with browser storage APIs
  • Interactive UI: Creating responsive, user-friendly interfaces
  • State Architecture: Organizing state in scalable ways
  • Error Handling: Graceful handling of user errors and edge cases

Next Steps:

Your task manager demonstrates solid understanding of React state management. In the next lesson, you'll learn about the useEffect hook in more detail, including data fetching from APIs, handling side effects, and managing component lifecycles - building on the state management foundation you've established here.

Time Completed: ~45 minutes

Bonus Challenges (Optional):

If you finish early, try these additional features:

  1. Add task categories/tags with filtering by category
  2. Implement drag-and-drop reordering of tasks
  3. Add due date notifications for upcoming deadlines
  4. Create task search functionality to find specific tasks
  5. Add bulk actions like "mark all as complete"
  6. Implement task archiving instead of deletion
  7. Add task priority sorting and visual indicators