Module 3.4: Mini Project 2 - Interactive Menu Manager

(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
Appcomponent. - Timed UI Actions: Implementing a time-limited “Undo” functionality.
1. Design & Structure
We’ll structure the application with the following component hierarchy:

(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:
MenuApp(Parent): Manages themenuItemsandundoItemstate.MenuItem: Displays individual menu items, allowing price editing and deletion.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
handlePriceUpdatedown toMenuItemvia theonUpdatePriceprop.
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:
- We need a
deletedItemstate to temporarily store the last deleted item. - Use
setTimeoutto automatically cleardeletedItem.
// 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:
- CRUD Operations: Performing full Create, Read, Update, and Delete operations on a state array.
- Immutability: Always creating new arrays (
filter,map, spread...) instead of directly modifying the old one. - Component Communication: Seamless collaboration between the Parent (logic) and Child (UI) components.
- Temporary State: Using temporary state and
setTimeoutto create modern user experiences (Undo functionality).
Congratulations on completing Module 3! Get ready for Module 4, where we’ll dive into Effects & Data Fetching.