Module 7.5: Mini Project 6 - E-commerce Storefront

(AI Image Gen): > Prompt: A vibrant, bustling digital storefront viewed from a 3D isometric angle. The shop windows display various products (gadgets, clothes). Floating icons represent core features: a magnifying glass (Search), a funnel (Filter), and a shopping cart overflowing with items. Customers (users) are navigating smooth pathways into the store. Color palette: Energetic Orange and Trustworthy Blue.
🛠️ Image Assets Script
Run the following script in your terminal to create placeholder image files:
mkdir -p images/b07-5
touch images/b07-5/b07-5-banner.png
touch images/b07-5/b07-5-sitemap.png
touch images/b07-5/b07-5-filter-demo.png🎯 Learning Objectives
In this project, we’ll synthesize our knowledge of Routing to build a mini online storefront, complete with advanced navigation features.
Key features:
- Product Listing: Filter and Sort products, synchronizing the
stateto the URL (useSearchParams). - Product Detail: Dynamic product detail page (
/products/:id). - Shopping Cart: Simple shopping cart page.
- Navigation: Menu and breadcrumb navigation system.
1. Project Structure (Sitemap)

(AI Image Gen): > Prompt: A hierarchical sitemap diagram. Root: “Home Page”. Branch 1: “Shop” -> “Product Detail”. Branch 2: “Cart”. Branch 3: “Checkout” (Protected Route). Visual style: Corporate blueprint, clear connection lines.
/: Home Page (Banner, Featured Products)./shop: Product Listing Page (with Filter Sidebar)./shop/:id: Detail Page./cart: Cart.
2. Mock Data & Setup
Create the file src/data.js to simulate a database.
// src/data.ts
export interface Product {
id: number;
name: string;
category: string;
price: number;
image: string;
}
export const PRODUCTS: Product[] = [
{ id: 1, name: 'Mechanical Keyboard', category: 'electronics', price: 120, image: '⌨️' },
{ id: 2, name: 'Gaming Mouse', category: 'electronics', price: 60, image: '🖱️' },
{ id: 3, name: 'Cotton T-Shirt', category: 'clothing', price: 25, image: '👕' },
{ id: 4, name: 'Denim Jeans', category: 'clothing', price: 50, image: '👖' },
{ id: 5, name: 'Coffee Mug', category: 'home', price: 15, image: '☕' },
];3. Building the Shop Page (Filter Sync URL)
This is the most crucial section, applying your knowledge of useSearchParams.
File: src/pages/Shop.jsx
// src/pages/Shop.tsx
import { useSearchParams, Link } from 'react-router-dom';
import { PRODUCTS } from '../data';
export default function Shop() {
const [searchParams, setSearchParams] = useSearchParams();
// 1. Đọc filter từ URL
const categoryFilter = searchParams.get('category') || 'all';
const sortOrder = searchParams.get('sort') || 'asc';
// 2. Hàm xử lý update URL
const updateFilter = (key: string, value: string) => {
const newParams = new URLSearchParams(searchParams);
if (value === 'all') {
newParams.delete(key);
} else {
newParams.set(key, value);
}
setSearchParams(newParams);
};
// 3. Logic lọc và sắp xếp
const filteredProducts = PRODUCTS.filter((p) =>
categoryFilter === 'all' ? true : p.category === categoryFilter
).sort((a, b) => (sortOrder === 'asc' ? a.price - b.price : b.price - a.price));
return (
<div className="flex gap-8 p-8">
{/* Sidebar Controls */}
<aside className="w-64 space-y-6">
<div>
<h3 className="font-bold mb-2">Category</h3>
<div className="flex flex-col gap-2">
{['all', 'electronics', 'clothing', 'home'].map((cat) => (
<label key={cat} className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="category"
checked={categoryFilter === cat}
onChange={() => updateFilter('category', cat)}
/>
<span className="capitalize">{cat}</span>
</label>
))}
</div>
</div>
<div>
<h3 className="font-bold mb-2">Sort Price</h3>
<select
value={sortOrder}
onChange={(e) => updateFilter('sort', e.target.value)}
className="w-full border p-2 rounded"
>
<option value="asc">Low to High</option>
<option value="desc">High to Low</option>
</select>
</div>
</aside>
{/* Product Grid */}
<main className="flex-1">
<h2 className="text-2xl font-bold mb-4">Products ({filteredProducts.length})</h2>
<div className="grid grid-cols-3 gap-6">
{filteredProducts.map((product) => (
<div key={product.id} className="border p-4 rounded-xl shadow hover:shadow-lg transition">
<div className="text-4xl mb-4 text-center">{product.image}</div>
<h3 className="font-bold text-lg">{product.name}</h3>
<p className="text-gray-500 capitalize">{product.category}</p>
<div className="flex justify-between items-center mt-4">
<span className="text-green-600 font-bold text-xl">${product.price}</span>
<Link
to={`/shop/${product.id}`}
className="bg-blue-600 text-white px-4 py-2 rounded text-sm hover:bg-blue-700"
>
View
</Link>
</div>
</div>
))}
</div>
{filteredProducts.length === 0 && (
<div className="text-center text-gray-400 py-10">No products found matching your filters.</div>
)}
</main>
</div>
);
}
(AI Image Gen): > Prompt: A split screen. Top half: The Browser URL bar showing
.../shop?category=electronics&sort=desc. Bottom half: The Product Grid showing only electronic gadgets sorted by price. Connecting lines show the relationship between the URL parameters and the visible UI elements.
4. Building the Product Detail Page
This page uses useParams to fetch the ID and useNavigate to create a Back button.
File: src/pages/ProductDetail.jsx
// src/pages/ProductDetail.tsx
import { useParams, useNavigate } from 'react-router-dom';
import { PRODUCTS } from '../data';
export default function ProductDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// Find the product (Note: URL parameters are always strings)
const product = PRODUCTS.find((p) => p.id === parseInt(id || '0'));
if (!product) {
return <div className="p-8 text-center text-red-500">Product not found!</div>;
}
return (
<div className="max-w-4xl mx-auto p-8">
<button
onClick={() => navigate(-1)} // Go back to the previous page
className="mb-4 text-gray-500 hover:text-black"
>
← Back
</button>
<div className="grid grid-cols-2 gap-10">
<div className="bg-gray-100 rounded-2xl flex items-center justify-center text-9xl h-96">{product.image}</div>
<div>
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-bold uppercase tracking-wide">
{product.category}
</span>
<h1 className="text-4xl font-bold mt-4 mb-2">{product.name}</h1>
<p className="text-3xl text-green-600 font-bold mb-6">${product.price}</p>
<p className="text-gray-600 mb-8 leading-relaxed">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua.
</p>
<button
onClick={() => alert(`Added ${product.name} to cart!`)}
className="w-full bg-black text-white py-4 rounded-xl font-bold text-lg hover:bg-gray-800 transition"
>
Add to Cart
</button>
</div>
</div>
</div>
);
}5. Routing Config (App.jsx)
Finally, let’s assemble everything in App.jsx.
// App.tsx
import { Routes, Route, NavLink } from 'react-router-dom';
import Shop from './pages/Shop';
import ProductDetail from './pages/ProductDetail';
// Simple Layout
const Navbar = () => (
<nav className="flex gap-6 p-4 border-b justify-center">
<NavLink to="/" className={({ isActive }) => (isActive ? 'font-bold' : '')}>
Home
</NavLink>
<NavLink to="/shop" className={({ isActive }) => (isActive ? 'font-bold' : '')}>
Shop
</NavLink>
<NavLink to="/cart" className={({ isActive }) => (isActive ? 'font-bold' : '')}>
Cart
</NavLink>
</nav>
);
export default function App() {
return (
<div>
<Navbar />
<Routes>
<Route path="/" element={<h1 className="text-center mt-10 text-3xl">Welcome to React Store! 🛍️</h1>} />
<Route path="/shop" element={<Shop />} />
<Route path="/shop/:id" element={<ProductDetail />} />
<Route path="/cart" element={<h1 className="text-center mt-10">Cart Page (Coming Soon)</h1>} />
<Route path="*" element={<h1 className="text-center mt-10 text-red-500">404 Not Found</h1>} />
</Routes>
</div>
);
}6. Summary
This project helped you practice:
- URL Sync: Synchronizing Category and Sort Price filters to the URL, ensuring the
statepersists on refresh. - Dynamic Routing: Handling product detail pages based on ID.
- Navigation: Using
useNavigatefor backward navigation (-1).
7. Challenges (Optional)
- Add a Search feature to the Shop page (synchronizing the search keyword to the URL
?q=...). - Create a
CartContextto store the actual shopping cart and display the item count on the Navbar.