Restaurant menu & ordering page
A mobile-first menu where customers browse dishes and send an order over WhatsApp.
- Next.js
- Tailwind
- Vercel
Telugu lo nerchuko · Watch in Telugu
A small restaurant rarely needs a full ordering platform with payments and logins. It needs a clean menu on the phone and a way for the customer to send the order. This project builds exactly that — and teaches client state and the WhatsApp deep-link trick you will reuse in the bot projects.
Overview
You will build a mobile-first menu page. Customers tap dishes to add them to a cart, see a running total, and hit one button that opens WhatsApp with the order pre-filled to the restaurant's number. No backend, no payment gateway — the order arrives as a message the owner already knows how to handle.
The problem
Owners want orders without learning new software, and customers want to order without installing anything. Payment gateways add cost and KYC friction a tiny shop may not want yet. A pre-filled WhatsApp message meets both sides where they already are.
Architecture
A static Next.js page holds menu data and cart state in the browser. On checkout, it builds a wa.me URL with the order text and opens it. Everything runs client-side.
Customer phone
tap dishes --> cart state (React) --> "Order on WhatsApp" button
|
wa.me/<number>?text=<order>
|
Owner's WhatsAppTech stack
- Next.js + Tailwind for the page and styling
- React state for the cart (no store needed at this size)
- WhatsApp click-to-chat (
wa.me) for order delivery - Vercel for hosting
Build steps
- Model the menu and a cart hook.
// data/menu.ts
export type Item = { id: string; name: string; price: number; veg: boolean };
export const menu: Item[] = [
{ id: "dosa", name: "Ghee dosa", price: 90, veg: true },
{ id: "idli", name: "Idli (2 pc)", price: 50, veg: true },
];- Track quantities in component state.
"use client";
import { useState } from "react";
import { menu } from "@/data/menu";
export default function Menu() {
const [cart, setCart] = useState<Record<string, number>>({});
const add = (id: string) =>
setCart((c) => ({ ...c, [id]: (c[id] ?? 0) + 1 }));
const total = menu.reduce((sum, i) => sum + (cart[i.id] ?? 0) * i.price, 0);- Build the WhatsApp order link from the cart.
const NUMBER = "919999999999"; // restaurant number, country code, no +
const lines = menu
.filter((i) => cart[i.id])
.map((i) => `${cart[i.id]} x ${i.name} = ₹${cart[i.id] * i.price}`);
const text = encodeURIComponent(
`New order:\n${lines.join("\n")}\n\nTotal: ₹${total}`
);
const waUrl = `https://wa.me/${NUMBER}?text=${text}`;- Render items and the sticky checkout bar.
return (
<main className="mx-auto max-w-md px-4 pb-24">
{menu.map((i) => (
<button key={i.id} onClick={() => add(i.id)}
className="flex w-full justify-between border-b py-3 text-left">
<span>{i.name} {i.veg && "🟢"}</span>
<span>₹{i.price} · {cart[i.id] ?? 0}</span>
</button>
))}
<a href={waUrl} target="_blank"
className="fixed inset-x-4 bottom-4 rounded-full bg-green-600 py-3 text-center text-white">
Order on WhatsApp · ₹{total}
</a>
</main>
);
}Source code
Starter: AnythingWithSandy/menu-starter. Swap the menu data and the phone number and it is ready for one restaurant.
Demo
No shared demo — deploy your own and open it on a phone to test the WhatsApp handoff end to end.
Deployment
npx vercel --prodSend the owner the URL. Updating the menu means editing data/menu.ts and pushing — Vercel redeploys on every push.
Bonus challenge
Add categories with tabs, a veg-only filter, and per-item notes (for example "less spicy") that flow into the WhatsApp message. Then move the menu data to a single JSON file the owner can edit without touching code.