Build the home and menu pages
Build the real Home and Menu pages for Brew & Bean: a menu data file, a rupee formatter, a reusable MenuCard, a hero with customer favourites, and a category filter. You will learn props, lists, keys and state, and how to review AI-written components.
In this lesson you replace the placeholder Home and Menu pages with the real thing. You will create the menu data file, a reusable MenuCard component, a Home page with a hero section and customer favourites, and a Menu page with category filter buttons. Along the way you will learn how React shows lists, how props work and why we keep the filter choice in state.
Start from: the project at the end of *Start the React project with Vite* (header, footer and four placeholder pages, committed to Git). The dev server should be running with npm run dev.
The plan for this lesson
Files you will create or change
| File | What it does |
|---|---|
| src/data/menu.js | The 8 menu items, the category list and the labels |
| src/utils/format.js | Formats a number as rupees: 180 becomes ₹180 |
| src/components/MenuCard.jsx | One menu item as a card |
| src/pages/Home.jsx | Hero, three favourites, info cards (replaces the placeholder) |
| src/pages/Menu.jsx | All items with category buttons (replaces the placeholder) |
| src/index.css | New styles added at the end |
Stage 1: The menu data
Everything on these pages comes from one list, so we create it first. Putting data in its own file means a component never contains hard-coded prices, and in the API lessons you will replace this file with a call to the server without changing the components.
Create a JavaScript file src/data/menu.js for a React app.
Export:
1. `menu`: an array of objects with the fields id, name, category, price (whole rupees, a number) and description (one short sentence, no marketing hype).
2. `categories`: ['all', 'coffee', 'tea', 'bakery', 'food']
3. `categoryLabels`: an object mapping each category to a display label, for example coffee: 'Coffee'.
Use exactly these items (id, name, category, price): espresso, Espresso, coffee, 120; cappuccino, Cappuccino, coffee, 180; cold-brew, Cold Brew, coffee, 220; filter-coffee, Filter Coffee, coffee, 80; masala-chai, Masala Chai, tea, 90; croissant, Butter Croissant, bakery, 150; banana-bread, Banana Bread, bakery, 130; veg-sandwich, Veg Grilled Sandwich, food, 160.
Use ES module exports. No other code.What to expect: Check every id, name, category and price against the list. AI often changes a price or invents an extra item. Check that ids are lowercase with hyphens and unique, and that prices are numbers, not strings like '₹120'.
export const menu = [
{
id: 'espresso',
name: 'Espresso',
category: 'coffee',
price: 120,
description: 'A short, strong shot made from freshly ground beans.',
},
{
id: 'cappuccino',
name: 'Cappuccino',
category: 'coffee',
price: 180,
description: 'Espresso with steamed milk and a thick layer of foam.',
},
{
id: 'cold-brew',
name: 'Cold Brew',
category: 'coffee',
price: 220,
description: 'Coffee steeped slowly in cold water and served over ice.',
},
{
id: 'filter-coffee',
name: 'Filter Coffee',
category: 'coffee',
price: 80,
description: 'South Indian style filter coffee with hot, frothy milk.',
},
{
id: 'masala-chai',
name: 'Masala Chai',
category: 'tea',
price: 90,
description: 'Black tea simmered with milk, ginger and warming spices.',
},
{
id: 'croissant',
name: 'Butter Croissant',
category: 'bakery',
price: 150,
description: 'Flaky, golden and baked fresh every morning.',
},
{
id: 'banana-bread',
name: 'Banana Bread',
category: 'bakery',
price: 130,
description: 'A soft, moist slice made with ripe bananas.',
},
{
id: 'veg-sandwich',
name: 'Veg Grilled Sandwich',
category: 'food',
price: 160,
description: 'Grilled vegetables and cheese between toasted bread.',
},
]
export const categories = ['all', 'coffee', 'tea', 'bakery', 'food']
export const categoryLabels = {
all: 'All',
coffee: 'Coffee',
tea: 'Tea',
bakery: 'Bakery',
food: 'Food',
}Prices are plain numbers. The ₹ sign is added when we display them.
Why prices are numbers and not text: you can add numbers (for the cart total) but you cannot add "₹120" and "₹180". We format for display at the last moment.
Stage 2: Formatting prices
A tiny helper turns 180 into ₹180. It uses Intl.NumberFormat, which is built into the browser. With the en-IN locale it also groups digits the Indian way (for example, 120000 shows as ₹1,20,000). Writing this once means every page shows prices the same way.
const rupees = new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
maximumFractionDigits: 0,
})
export function formatPrice(amount) {
return rupees.format(amount)
}The formatter is created once at the top, not on every call. maximumFractionDigits: 0 hides paise because our prices are whole rupees.
Stage 3: The MenuCard component
A component is a function that returns what to show. It receives inputs called props. MenuCard receives one prop, item, and shows its category, name, description and price. We use it on both Home and Menu, so we write it once.
The coloured square with the first letter is a stand-in for a photo. It keeps the site fast and avoids image licence problems. Because it is only decoration, it has aria-hidden="true" so screen readers skip it.
Write a React component `MenuCard` in src/components/MenuCard.jsx.
Props: `item` with id, name, category, price, description.
It renders an <article> with: a decorative square (aria-hidden) showing the first letter of the name, the category label, the name as an <h3>, the description, and the price.
Import `categoryLabels` from '../data/menu.js' and `formatPrice` from '../utils/format.js'.
Use class names: menu-card, swatch, menu-card-body, category-tag, menu-card-footer, price.
No inline styles and no extra libraries. Plain function component with a default export.What to expect: Check: it is an <article>, the name is a heading, the swatch is hidden from screen readers, there are no inline styles, and the import paths end in .js. There is no Add button yet. It comes in the cart lesson.
import { categoryLabels } from '../data/menu.js'
import { formatPrice } from '../utils/format.js'
export default function MenuCard({ item }) {
return (
<article className="menu-card">
<div className="swatch" aria-hidden="true">
{item.name.charAt(0)}
</div>
<div className="menu-card-body">
<span className="category-tag">{categoryLabels[item.category]}</span>
<h3>{item.name}</h3>
<p>{item.description}</p>
<div className="menu-card-footer">
<span className="price">{formatPrice(item.price)}</span>
</div>
</div>
</article>
)
}The footer div wraps only the price today. In the cart lesson the Add button goes next to it.
Stage 4: The Home page
The Home page has three parts: a hero with two buttons, the customer favourites, and three info cards. For the favourites we keep a list of ids and filter the menu with it, so the text and price always come from the one source of truth.
Notice popular.map(...). React shows a list by mapping an array to elements. Each element needs a key prop with a unique, stable value (here the item id), so React can tell the items apart when the list changes. Never use the array index as a key if the list can be reordered or filtered.
import { Link } from 'react-router-dom'
import MenuCard from '../components/MenuCard.jsx'
import { menu } from '../data/menu.js'
const popularIds = ['cappuccino', 'cold-brew', 'croissant']
export default function Home() {
const popular = menu.filter((item) => popularIds.includes(item.id))
return (
<>
<section className="hero">
<div className="container">
<h1>Good coffee, ready when you are</h1>
<p>
Brew & Bean is a small coffee shop in Indiranagar, Bengaluru.
Order ahead and pick up your coffee, chai and bakes without the
wait.
</p>
<div className="hero-actions">
<Link to="/menu" className="btn">
See the menu
</Link>
<Link to="/cart" className="btn btn-secondary">
View your cart
</Link>
</div>
</div>
</section>
<section className="container page">
<h2>Customer favourites</h2>
<ul className="menu-grid">
{popular.map((item) => (
<li key={item.id}>
<MenuCard item={item} />
</li>
))}
</ul>
</section>
<section className="container page">
<h2>Visit us</h2>
<div className="info-grid">
<div className="info-card">
<h3>Opening hours</h3>
<p>Open 8 am to 10 pm.</p>
</div>
<div className="info-card">
<h3>Find us</h3>
<p>Indiranagar, Bengaluru.</p>
</div>
<div className="info-card">
<h3>Pickup only</h3>
<p>Order here and collect at the counter. We do not deliver.</p>
</div>
</div>
</section>
</>
)
}The <> ... </> wrapper is a fragment: it lets us return several sections without adding an extra div.
Stage 5: The Menu page with a filter
Now the Menu page. The selected category is state: a value that can change and that makes React redraw the page when it does. useState('all') gives us the current value and a function to change it.
One good habit: don't store the filtered list in state. Store only the choice (category) and calculate visibleItems from it on every render. If you store both, they can get out of sync. This is called derived state, and it prevents a whole class of bugs.
The buttons use aria-pressed so screen readers announce which filter is on.
Write a React page component `Menu` in src/pages/Menu.jsx.
It imports { menu, categories, categoryLabels } from '../data/menu.js' and MenuCard from '../components/MenuCard.jsx'.
It keeps the selected category in useState (default 'all').
It shows an <h1> 'Our menu', then a row of <button type="button"> filter buttons (one per category, labelled with categoryLabels, with aria-pressed), then a <ul> of MenuCards for the items in the selected category.
Do not store the filtered list in state: compute it from the selected category.
Class names: container page, filters, chip, chip-active, menu-grid.What to expect: Check: only the category is in state; each list item has key={item.id}; buttons have type="button"; there is no useEffect (you don't need one for this).
import { useState } from 'react'
import MenuCard from '../components/MenuCard.jsx'
import { menu, categories, categoryLabels } from '../data/menu.js'
export default function Menu() {
const [category, setCategory] = useState('all')
const visibleItems =
category === 'all' ? menu : menu.filter((item) => item.category === category)
return (
<section className="container page">
<h1>Our menu</h1>
<div className="filters" role="group" aria-label="Filter by category">
{categories.map((name) => (
<button
key={name}
type="button"
className={name === category ? 'chip chip-active' : 'chip'}
aria-pressed={name === category}
onClick={() => setCategory(name)}
>
{categoryLabels[name]}
</button>
))}
</div>
<ul className="menu-grid">
{visibleItems.map((item) => (
<li key={item.id}>
<MenuCard item={item} />
</li>
))}
</ul>
</section>
)
}Clicking a chip calls setCategory, React re-runs the function, and visibleItems is recalculated.
Stage 6: The styles
Add these rules to the end of src/index.css (don't delete what is already there). They style the buttons, hero, info cards, filter chips and the card grid. We wrote them desktop-first with three fixed columns on purpose. In the polish lesson you will test on a phone and fix them.
/* Buttons */
.btn {
display: inline-block;
background: var(--brand);
color: #ffffff;
border: 0;
border-radius: 8px;
padding: 10px 20px;
font: inherit;
font-weight: 600;
text-decoration: none;
cursor: pointer;
}
.btn:hover {
background: var(--brand-dark);
}
.btn-secondary {
background: transparent;
color: var(--brand);
border: 2px solid var(--brand);
}
.btn-secondary:hover {
background: var(--accent);
}
/* Home page */
.hero {
background: var(--accent);
padding: 64px 0;
}
.hero h1 {
font-size: 3rem;
margin: 0 0 16px;
}
.hero p {
font-size: 1.2rem;
max-width: 36rem;
margin: 0 0 24px;
}
.hero-actions {
display: flex;
gap: 12px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.info-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 20px;
}
.info-card h3 {
margin: 0 0 4px;
}
.info-card p {
margin: 0;
color: var(--muted);
}
/* Menu */
.filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.chip {
border: 1px solid var(--brand);
background: var(--surface);
color: var(--brand);
border-radius: 999px;
padding: 6px 16px;
font: inherit;
cursor: pointer;
}
.chip-active {
background: var(--brand);
color: #ffffff;
}
.menu-grid {
list-style: none;
margin: 24px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.menu-card {
display: flex;
flex-direction: column;
height: 100%;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.swatch {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 700;
color: var(--brand);
background: var(--accent);
}
.menu-card-body {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
padding: 16px;
}
.menu-card h3 {
margin: 0;
font-size: 1.15rem;
}
.menu-card p {
margin: 0;
flex: 1;
color: var(--muted);
}
.category-tag {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.menu-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price {
font-weight: 700;
}Colours come from the variables defined at the top of the file.
Test it
What to check
- 1Save everything. The browser should refresh by itself.
- 2Open Home. You should see a beige hero section with a heading and two buttons, then three cards (Cappuccino, Cold Brew, Butter Croissant) and three info cards.
- 3Click "See the menu". The Menu page shows eight cards in three columns and a row of buttons: All, Coffee, Tea, Bakery, Food.
- 4Click Tea. Only Masala Chai remains. Click Coffee: four cards. Click All: eight.
- 5Check the prices: Cappuccino ₹180, Filter Coffee ₹80, Cold Brew ₹220.
- 6Open the browser console (F12). There should be no red errors and no "key" warnings.
Menu page, Coffee selected:
Espresso ₹120 | Cappuccino ₹180 | Cold Brew ₹220
Filter Coffee ₹80
(the Coffee button is dark brown with white text; the others are white)Done when
- Home and Menu both show cards from the same menu.js file.
- Changing a price in menu.js changes it on both pages after saving.
- All five filter buttons work and the active one looks different.
- No red errors in the console.
Commit your work:
git add .
git commit -m "Add menu data, home page and menu page"Review what the AI gave you
When you ask AI for components, these are the mistakes to look for. Making a habit of this check is more valuable than the code itself.
Common problems in AI-written components
| Look for | Why it matters | What to do |
|---|---|---|
| Prices or items different from your list | Wrong data goes straight onto the site | Compare line by line with the spec |
| key={index} on a list that can change | Wrong items can be reused or reordered wrongly | Use key={item.id} |
| Filtered list stored in useState | Two sources of truth that drift apart | Store only the choice; compute the list |
| A div with onClick instead of a button | Not reachable by keyboard | Use a real button |
| Inline styles everywhere | Hard to change and repeat | Use class names and the CSS file |
| New packages (UI kits, icon packs) | More to learn, more to break | Say "no extra libraries" in the prompt |
Try it: Add a search box
Add a text input above the filter buttons that filters the menu by name as you type (for example "cap" shows only Cappuccino). It should work together with the category filter.
Try it: Stretch: mark one item as a bestseller
Add bestseller: true to two items in menu.js and show a small "Bestseller" label on those cards. Then use it on the Home page instead of the hard-coded popularIds list.
Key takeaways
- Keep data in one file (
src/data/menu.js) and let components read from it. - Props pass data into a component;
mapwith a uniquekeyshows a list. - State holds the one thing that changes (the selected category); compute everything else from it.
- Store prices as numbers and format them for display with
Intl.NumberFormat('en-IN'). - Use real buttons and headings so the page works with a keyboard and a screen reader.
- Always compare AI-written data with your source, and look for the common mistakes in the table.
Check yourself
Finishing the quiz marks the lesson as done.
1. Why is the filtered list calculated on each render instead of being stored in state?
2. What is the key prop for in menu.map(item => <li key={item.id}>...)?
3. Why store price as 180 and not as '₹180'?
4. The AI wrote a filter button as <div onClick={...}>. What is the problem?
5. Why does the swatch square have aria-hidden="true"?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.