Back to Full-Stack Mastery

Module 1: Modern Frontend Development

Build a complete Task Management App with React, Next.js, and TypeScript

šŸ“š Understanding Frontend Development

Frontend development is the practice of building the user interface (UI) and user experience (UX) of web applications. It's what users see and interact with in their browsers.

The Three Pillars of Frontend

HTML

Structure and content - the skeleton of your webpage

CSS

Styling and layout - the appearance and design

JavaScript

Interactivity and logic - the behavior and functionality

Modern Frontend Stack

Today's frontend development has evolved beyond vanilla HTML, CSS, and JavaScript. We use powerful frameworks and tools that make development faster, more maintainable, and scalable.

React

A JavaScript library for building user interfaces using reusable components. Think of components as LEGO blocks - you build complex UIs by combining smaller, reusable pieces.

Next.js

A React framework that adds powerful features like server-side rendering, routing, and optimization out of the box. It's like React with superpowers.

TypeScript

JavaScript with type safety. It catches errors before you run your code and makes your code more predictable and easier to maintain.

Tailwind CSS

A utility-first CSS framework that lets you style elements directly in your HTML/JSX using pre-defined classes. No need to write custom CSS files.

šŸŽÆ What We'll Build

A fully functional Task Management Application. This project will teach you the fundamentals of React, state management, component composition, and TypeScript - all essential skills for modern frontend development.

Features:

  • Create, edit, and delete tasks
  • Mark tasks as complete/incomplete
  • Filter tasks by status (All, Active, Completed)
  • Responsive design with Tailwind CSS
  • Type-safe with TypeScript

What You'll Learn:

• React components and props
• State management with useState
• Event handling in React
• TypeScript interfaces and types
• Array methods (map, filter)
• Conditional rendering
• Form handling and validation
• Tailwind CSS utility classes

Lesson 1: Project Setup

šŸ“– Understanding the Setup

Before we write any code, we need to set up our development environment. Next.js provides a command-line tool that creates a new project with all the necessary configuration files and folder structure.

Think of this like setting up a kitchen before cooking - you need the right tools, ingredients, and workspace organized before you start.

Step 1: Create Next.js Project

Open your terminal and run the following command. This will create a new Next.js project with TypeScript, Tailwind CSS, and other modern tools pre-configured.

npx create-next-app@latest task-manager

# When prompted, choose these options:
āœ“ Would you like to use TypeScript? Yes
āœ“ Would you like to use ESLint? Yes
āœ“ Would you like to use Tailwind CSS? Yes
āœ“ Would you like to use App Router? Yes
āœ“ Would you like to customize the default import alias? No

cd task-manager
npm run dev

šŸ’” What's happening? This command downloads and sets up a complete Next.js project with TypeScript for type safety, Tailwind for styling, and the App Router for modern routing.

Step 2: Understanding Project Structure

After setup, your project will have this structure. Let's understand what each folder does:

task-manager/
ā”œā”€ā”€ app/              # Your application pages and layouts
│   ā”œā”€ā”€ layout.tsx    # Root layout (wraps all pages)
│   ā”œā”€ā”€ page.tsx      # Home page (main entry point)
│   └── globals.css   # Global styles
ā”œā”€ā”€ components/       # Reusable UI components (we'll create this)
ā”œā”€ā”€ types/           # TypeScript type definitions (we'll create this)
ā”œā”€ā”€ public/          # Static files (images, fonts, etc.)
ā”œā”€ā”€ node_modules/    # Installed dependencies
└── package.json     # Project configuration and dependencies

šŸ“š Key Concepts:

app/ - Contains your pages. Each folder becomes a route in your app.

components/ - Reusable pieces of UI that you can use across multiple pages.

types/ - TypeScript definitions that describe the shape of your data.

Step 3: Create Folders and Define Types

First, let's create the folders we'll need and define our Task type. A "type" in TypeScript is like a blueprint that describes what properties an object should have.

Create the folders:

mkdir components
mkdir types

Create types/task.ts:

export interface Task {
  id: string;           // Unique identifier for each task
  title: string;        // Task name/title
  description: string;  // Optional details about the task
  completed: boolean;   // Is the task done? true or false
  createdAt: Date;      // When was the task created
}

šŸ’” Why TypeScript?

TypeScript helps catch errors before you run your code. If you try to use a Task object without an 'id' property, TypeScript will warn you immediately. This saves hours of debugging!

Lesson 2: Building the Task Form Component

šŸ“– Understanding React Components

In React, everything is a component. A component is a reusable piece of UI that can have its own logic and appearance. Think of components like custom HTML tags that you create.

For example, instead of writing the same form HTML everywhere, you create a TaskForm component and reuse it wherever you need a task form.

šŸ“– Understanding State

"State" is data that can change over time. In our form, the text you type is state - it changes as you type. React's useState hook lets us store and update this changing data.

Example: When you type "Buy groceries" in the input field, useState stores that text. When you type more, useState updates it. When you submit, we use that stored text to create a new task.

Step 1: Create the TaskForm Component

Create a new file components/TaskForm.tsx and add this code:

'use client';

import { useState } from 'react';
import { Task } from '@/types/task';

interface TaskFormProps {
  onAddTask: (task: Omit<Task, 'id' | 'createdAt'>) => void;
}

export default function TaskForm({ onAddTask }: TaskFormProps) {
  // State to store the task title as user types
  const [title, setTitle] = useState('');
  
  // State to store the task description as user types
  const [description, setDescription] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();  // Prevent page reload on form submit
    
    // Don't create empty tasks
    if (!title.trim()) return;

    // Call the parent component's function to add the task
    onAddTask({
      title: title.trim(),
      description: description.trim(),
      completed: false,
    });

    // Clear the form after submission
    setTitle('');
    setDescription('');
  };

  return (
    <form onSubmit={handleSubmit} className="mb-8 space-y-4">
      <div>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Task title..."
          className="w-full px-4 py-2 border rounded-lg 
                     focus:outline-none focus:ring-2 focus:ring-blue-500
                     dark:bg-gray-800 dark:border-gray-700"
        />
      </div>
      
      <div>
        <textarea
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          placeholder="Task description (optional)..."
          rows={3}
          className="w-full px-4 py-2 border rounded-lg 
                     focus:outline-none focus:ring-2 focus:ring-blue-500
                     dark:bg-gray-800 dark:border-gray-700"
        />
      </div>
      
      <button
        type="submit"
        className="w-full bg-blue-500 hover:bg-blue-600 
                   text-white px-4 py-2 rounded-lg 
                   transition-colors font-medium"
      >
        Add Task
      </button>
    </form>
  );
}

šŸ’” Code Breakdown:

'use client'

Tells Next.js this component runs in the browser (client-side) because it uses interactive features like useState.

useState('')

Creates a state variable initialized with an empty string. Returns [value, setValue] - the current value and a function to update it.

onChange handler

Runs every time the user types. Updates the state with the new input value.

e.preventDefault()

Stops the form from reloading the page (default browser behavior).

Tailwind classes

Utility classes for styling: w-full (full width), px-4 (padding horizontal), rounded-lg (rounded corners), etc.

šŸŽÆ What You Learned:

  • āœ“ How to create a React component
  • āœ“ How to use useState to manage form data
  • āœ“ How to handle form submission
  • āœ“ How to pass data to parent components via props
  • āœ“ How to style with Tailwind CSS

Lesson 3: Task Item Component

šŸ“– Component Composition

Component composition is building complex UIs by combining smaller components. Our TaskItem component will display a single task, and we'll use multiple TaskItem components to show a list.

This is like building with LEGO - each TaskItem is a brick, and we stack them to create the full list.

Step 1: Create TaskItem Component

Create components/TaskItem.tsx:

'use client';

import { Task } from '@/types/task';

interface TaskItemProps {
  task: Task;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}

export default function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <div className="bg-white dark:bg-gray-800 border rounded-lg p-4 
                    hover:shadow-md transition-shadow">
      <div className="flex items-start gap-3">
        {/* Checkbox to mark task complete/incomplete */}
        <input
          type="checkbox"
          checked={task.completed}
          onChange={() => onToggle(task.id)}
          className="mt-1 h-5 w-5 rounded border-gray-300 
                     text-blue-500 focus:ring-blue-500 cursor-pointer"
        />
        
        <div className="flex-1">
          {/* Task title with strikethrough if completed */}
          <h3 className={`text-lg font-semibold transition-all ${
            task.completed 
              ? 'line-through text-gray-400' 
              : 'text-gray-900 dark:text-gray-100'
          }`}>
            {task.title}
          </h3>
          
          {/* Show description if it exists */}
          {task.description && (
            <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
              {task.description}
            </p>
          )}
          
          {/* Show when task was created */}
          <p className="text-xs text-gray-400 mt-2">
            Created: {new Date(task.createdAt).toLocaleDateString()}
          </p>
        </div>
        
        {/* Delete button */}
        <button
          onClick={() => onDelete(task.id)}
          className="text-red-500 hover:text-red-700 px-3 py-1 rounded 
                     hover:bg-red-50 dark:hover:bg-red-950 
                     transition-colors text-sm font-medium"
        >
          Delete
        </button>
      </div>
    </div>
  );
}

šŸ’” Code Breakdown:

Props Destructuring

We receive task, onToggle, and onDelete from the parent component. This is how components communicate.

Conditional Rendering

The {task.description && <p>...}</p>} only shows the description if it exists (not empty).

Conditional Styling

The title gets a line-through style when task.completed is true. This is dynamic styling based on state.

Event Handlers

onClick and onChange call functions passed from the parent, allowing the parent to update the task list.

šŸŽÆ What You Learned:

  • āœ“ How to receive and use props in components
  • āœ“ Conditional rendering with && operator
  • āœ“ Dynamic styling based on state
  • āœ“ Event handling (onClick, onChange)
  • āœ“ Date formatting in JavaScript

Lesson 4: Putting It All Together - Main App

šŸ“– Understanding State Management

The main app component manages the "source of truth" - the complete list of tasks. All other components receive data from here and send updates back up. This is called "lifting state up".

Think of it like this: The main app is the manager who keeps the master list. TaskForm tells the manager "add this task", TaskItem tells the manager "mark this complete" or "delete this". The manager updates the list and tells everyone about the changes.

Step 1: Update app/page.tsx

Replace the content of app/page.tsx with:

'use client';

import { useState } from 'react';
import TaskForm from '@/components/TaskForm';
import TaskItem from '@/components/TaskItem';
import { Task } from '@/types/task';

export default function Home() {
  // Main state: array of all tasks
  const [tasks, setTasks] = useState<Task[]>([]);
  
  // Filter state: which tasks to show
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

  // Function to add a new task
  const addTask = (taskData: Omit<Task, 'id' | 'createdAt'>) => {
    const newTask: Task = {
      ...taskData,
      id: crypto.randomUUID(),  // Generate unique ID
      createdAt: new Date(),     // Set creation time
    };
    setTasks([newTask, ...tasks]);  // Add to beginning of array
  };

  // Function to toggle task completion
  const toggleTask = (id: string) => {
    setTasks(tasks.map(task => 
      task.id === id 
        ? { ...task, completed: !task.completed }  // Toggle this task
        : task  // Keep other tasks unchanged
    ));
  };

  // Function to delete a task
  const deleteTask = (id: string) => {
    setTasks(tasks.filter(task => task.id !== id));
  };

  // Filter tasks based on current filter
  const filteredTasks = tasks.filter(task => {
    if (filter === 'active') return !task.completed;
    if (filter === 'completed') return task.completed;
    return true;  // 'all' shows everything
  });

  return (
    <main className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12">
      <div className="container mx-auto px-4 max-w-3xl">
        <h1 className="text-4xl font-bold mb-8 text-center 
                       bg-gradient-to-r from-blue-500 to-cyan-500 
                       bg-clip-text text-transparent">
          Task Manager
        </h1>
        
        {/* Task creation form */}
        <TaskForm onAddTask={addTask} />

        {/* Filter buttons */}
        <div className="flex gap-2 mb-6 flex-wrap">
          <button
            onClick={() => setFilter('all')}
            className={`px-4 py-2 rounded-lg transition-colors font-medium ${
              filter === 'all' 
                ? 'bg-blue-500 text-white' 
                : 'bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700'
            }`}
          >
            All ({tasks.length})
          </button>
          <button
            onClick={() => setFilter('active')}
            className={`px-4 py-2 rounded-lg transition-colors font-medium ${
              filter === 'active' 
                ? 'bg-blue-500 text-white' 
                : 'bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700'
            }`}
          >
            Active ({tasks.filter(t => !t.completed).length})
          </button>
          <button
            onClick={() => setFilter('completed')}
            className={`px-4 py-2 rounded-lg transition-colors font-medium ${
              filter === 'completed' 
                ? 'bg-blue-500 text-white' 
                : 'bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700'
            }`}
          >
            Completed ({tasks.filter(t => t.completed).length})
          </button>
        </div>

        {/* Task list */}
        <div className="space-y-3">
          {filteredTasks.length === 0 ? (
            <div className="text-center py-12 bg-white dark:bg-gray-800 
                            rounded-lg border-2 border-dashed">
              <p className="text-gray-500 text-lg">
                {filter === 'all' 
                  ? 'No tasks yet. Add one above!' 
                  : `No ${filter} tasks.`}
              </p>
            </div>
          ) : (
            filteredTasks.map(task => (
              <TaskItem
                key={task.id}
                task={task}
                onToggle={toggleTask}
                onDelete={deleteTask}
              />
            ))
          )}
        </div>
      </div>
    </main>
  );
}

šŸ’” Key Concepts Explained:

Array Methods

map() - Transform each item in an array. We use it to update a specific task.

filter() - Keep only items that match a condition. We use it to show/hide tasks and delete tasks.

Spread Operator (...)

Creates a copy of an object/array. {...task, completed: !task.completed} copies the task and changes only the completed property.

Conditional Rendering

Show different UI based on conditions. If no tasks, show a message. Otherwise, show the list.

Key Prop

React needs a unique "key" for each item in a list to track changes efficiently. We use task.id.

šŸŽ‰ Congratulations!

You've built a complete, functional task management app! Run npm run devand open http://localhost:3000 to see it in action.

What You've Learned:
āœ“ React components
āœ“ State management
āœ“ Props and data flow
āœ“ Event handling
āœ“ Array methods
āœ“ Conditional rendering
āœ“ TypeScript types
āœ“ Tailwind CSS

šŸš€ Next Steps

  • →Add local storage to persist tasks
  • →Implement task editing functionality
  • →Add due dates and priority levels
  • →Deploy to Vercel
Continue to Module 2: Backend Development →