Skip to Content
Module 3: State & Interactive UI🎯 Mini Project: Interactive Menu

Module 3.4: Mini Project 2 - Interactive Menu Manager

Banner

(AI Image Gen): > Prompt: A vibrant 3D isometric illustration of a digital restaurant menu interface floating in mid-air. The scene features interactive food cards with price tags, “Add” and “Delete” buttons floating nearby. A glowing “Undo” circular arrow icon hovers in the foreground. The background is a sleek, dark culinary theme with warm orange and charcoal gray accents. High-tech, clean, 8k resolution.

🛠️ Image Assets Script

Run the following script in your terminal to generate placeholder image files:

mkdir -p images/b03-4 touch images/b03-4/b03-4-banner.png touch images/b03-4/b03-4-app-structure.png

🎯 Learning Objectives

In this hands-on exercise, we’ll consolidate the knowledge from Module 3 to fully build the “Restaurant Menu Manager” application, complete with basic CRUD (Create, Read, Update, Delete) functionalities and advanced logic handling.

Skills you’ll hone:

  • State Array Management: Adding, editing, and deleting elements within your state array.
  • Lifting State Up: Centralizing state management in the App component.
  • Timed UI Actions: Implementing a time-limited “Undo” functionality.

1. Design & Structure

We’ll structure the application with the following component hierarchy:

App Structure

(AI Image Gen): > Prompt: A minimalist wireframe diagram of a Menu Manager App. Top section needs a “Header”. Middle section shows a “Menu List” with individual “MenuItem” rows (containing name, price input, delete button). Bottom section is an “Add Item Form”. A floating “Undo Toast” notification appears at the bottom right. Clean lines, blueprint style, blue and white color scheme.

Component Hierarchy:

  1. MenuApp (Parent): Manages the menuItems and undoItem state.
  2. MenuItem: Displays individual menu items, allowing price editing and deletion.
  3. AddItemForm: A form for adding new menu items.

2. Project Setup

2.1. Sample Data (Initial State)

Create a data.js file or declare it directly within MenuApp.

const INITIAL_MENU = [ { id: 1, name: 'Black Coffee', price: 25000 }, { id: 2, name: 'White Coffee', price: 30000 }, { id: 3, name: 'Passion Fruit Tea', price: 45000 }, ];

3. Feature Implementation (Step-by-Step)

3.1. Display List (Read) & Delete (Delete)

In MenuApp, render the list and pass the handleDelete function down.

import { useState } from 'react'; export default function MenuApp() { const [items, setItems] = useState(INITIAL_MENU); const handleDelete = (id) => { // Filter out the item with the matching id -> Creates a new array const newItems = items.filter((item) => item.id !== id); setItems(newItems); }; return ( <div className="w-96 mx-auto p-4 border rounded shadow-lg mt-10"> <h1 className="text-2xl font-bold mb-4 text-center">Menu Manager</h1> <div className="space-y-2"> {items.map((item) => ( <MenuItem key={item.id} item={item} onDelete={handleDelete} /> ))} </div> </div> ); }

Build the MenuItem component:

function MenuItem({ item, onDelete }) { return ( <div className="flex justify-between items-center p-2 bg-gray-50 rounded border"> <div> <h3 className="font-semibold">{item.name}</h3> <span className="text-gray-600">{item.price.toLocaleString()} đ</span> </div> <button onClick={() => onDelete(item.id)} className="text-red-500 hover:text-red-700 font-bold"> </button> </div> ); }

3.2. Price Update Functionality (Update)

Each menu item will have an input field for direct price editing. When the user types, the parent state must be updated.

Update MenuApp:

const handlePriceUpdate = (id, newPrice) => { // Map over the array, find the item with the matching id, and update the price const updatedItems = items.map((item) => { if (item.id === id) { return { ...item, price: Number(newPrice) }; // Create a new object } return item; // Keep the old item }); setItems(updatedItems); };

Update MenuItem:

function MenuItem({ item, onDelete, onUpdatePrice }) { return ( <div className="flex justify-between items-center p-2 bg-gray-50 rounded border"> <div className="w-1/2"> <h3 className="font-semibold">{item.name}</h3> </div> {/* Price update input */} <input type="number" className="w-20 border rounded px-1 text-right" value={item.price} onChange={(e) => onUpdatePrice(item.id, e.target.value)} /> <button onClick={() => onDelete(item.id)} className="ml-2 text-red-500 hover:text-red-700 font-bold"> </button> </div> ); }

Note: Remember to pass handlePriceUpdate down to MenuItem via the onUpdatePrice prop.

3.3. Add Item Functionality (Create)

Create an AddItemForm component to manage internal state for the input fields, then send the complete object to the parent.

import { useState } from 'react'; function AddItemForm({ onAddItem }) { const [name, setName] = useState(''); const [price, setPrice] = useState(''); const handleSubmit = (e) => { e.preventDefault(); if (!name || !price) return; // Send data to parent onAddItem({ id: Date.now(), // Simple random ID generation name, price: Number(price), }); // Reset form setName(''); setPrice(''); }; return ( <form onSubmit={handleSubmit} className="mt-4 flex gap-2"> <input className="border p-2 rounded flex-1" placeholder="Item name..." value={name} onChange={(e) => setName(e.target.value)} /> <input type="number" className="border p-2 rounded w-24" placeholder="Price..." value={price} onChange={(e) => setPrice(e.target.value)} /> <button className="bg-green-500 text-white px-4 py-2 rounded">+</button> </form> ); }

In MenuApp, add the handler function:

const handleAddItem = (newItem) => { setItems([...items, newItem]); };

4. Advanced Lab: Undo Functionality

Requirements: When an item is deleted, it shouldn’t disappear permanently right away. A notification will appear for 3 seconds with an “Undo” button. If “Undo” is clicked, the item reappears. After 3 seconds, the notification disappears, and the item is permanently deleted.

Logic:

  1. We need a deletedItem state to temporarily store the last deleted item.
  2. Use setTimeout to automatically clear deletedItem.
// Inside MenuApp const [deletedItem, setDeletedItem] = useState(null); const handleDelete = (id) => { // 1. Find the deleted item to store temporarily const itemToDelete = items.find((item) => item.id === id); setDeletedItem(itemToDelete); // 2. Remove from the main list setItems(items.filter((item) => item.id !== id)); // 3. Set a timeout to permanently delete (hide Undo button) after 3s setTimeout(() => { setDeletedItem(null); }, 3000); }; const handleUndo = () => { if (deletedItem) { // Add it back to the list setItems([...items, deletedItem]); // Hide the undo button immediately setDeletedItem(null); } };

UI for Undo Notification:

Add this snippet to the end of the MenuApp’s JSX:

{ deletedItem && ( <div className="fixed bottom-4 right-4 bg-gray-800 text-white p-4 rounded shadow-lg flex items-center gap-4 animate-bounce"> <span>Deleted "{deletedItem.name}"</span> <button onClick={handleUndo} className="bg-yellow-500 text-black px-3 py-1 rounded font-bold text-sm"> UNDO </button> </div> ); }

5. Conclusion

In this Mini Project, we’ve practiced:

  1. CRUD Operations: Performing full Create, Read, Update, and Delete operations on a state array.
  2. Immutability: Always creating new arrays (filter, map, spread ...) instead of directly modifying the old one.
  3. Component Communication: Seamless collaboration between the Parent (logic) and Child (UI) components.
  4. Temporary State: Using temporary state and setTimeout to create modern user experiences (Undo functionality).

Congratulations on completing Module 3! Get ready for Module 4, where we’ll dive into Effects & Data Fetching.

Last updated on