Build a beautiful digital menu system with photos, nutrition info, allergen warnings, and QR code access
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are an expert full-stack developer helping a Malaysian F&B business owner replace their printed menus with a beautiful, mobile-first digital menu system. The system must support tri-lingual display, nutritional information, allergen warnings, and QR code access — all without requiring customers to download an app or log in.
## Role & Context
Build a complete digital menu web application for a Malaysian restaurant or kopitiam called {restaurant_name}. The menu will be accessed by customers scanning a QR code at their table. The owner and kitchen staff will manage the menu through an admin panel. The system replaces printed menus and adds nutritional transparency that health-conscious customers increasingly expect.
## Tech Stack
- Next.js 14 (App Router) with TypeScript
- Tailwind CSS for styling, shadcn/ui for admin UI components
- Supabase (Postgres + Storage for photos + Auth for admin)
- Vercel for hosting
- next/image for optimized photo loading
- qrcode.react for QR code generation
- next-intl or a simple context-based language switcher for tri-lingual support
## Database Schema
Create the following tables in Supabase with full RLS policies:
sql
-- Menu categories
CREATE TABLE menu_categories (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name_en text NOT NULL,
name_zh text NOT NULL,
name_ms text NOT NULL,
icon text,
sort_order integer NOT NULL DEFAULT 0,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_menu_categories_sort ON menu_categories(sort_order);
-- Menu items
CREATE TABLE menu_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
category_id uuid NOT NULL REFERENCES menu_categories(id) ON DELETE RESTRICT,
name_en text NOT NULL,
name_zh text NOT NULL,
name_ms text NOT NULL,
description_en text,
description_zh text,
description_ms text,
price numeric(8,2) NOT NULL,
photo_url text,
is_available boolean NOT NULL DEFAULT true,
is_published boolean NOT NULL DEFAULT false,
spicy_level integer NOT NULL DEFAULT 0 CHECK (spicy_level BETWEEN 0 AND 3),
is_halal boolean NOT NULL DEFAULT false,
is_vegetarian boolean NOT NULL DEFAULT false,
prep_time_minutes integer,
sort_order integer NOT NULL DEFAULT 0,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX idx_menu_items_category ON menu_items(category_id);
CREATE INDEX idx_menu_items_available ON menu_items(is_available, is_published);
CREATE INDEX idx_menu_items_sort ON menu_items(category_id, sort_order);
-- Nutrition information (one row per menu item)
CREATE TABLE nutrition_info (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
item_id uuid NOT NULL UNIQUE REFERENCES menu_items(id) ON DELETE CASCADE,
calories integer,
protein_g numeric(6,1),
carbs_g numeric(6,1),
fat_g numeric(6,1),
sodium_mg integer,
sugar_g numeric(6,1),
serving_size_g integer,
updated_at timestamptz DEFAULT now()
);
CREATE INDEX idx_nutrition_item ON nutrition_info(item_id);
-- Allergens (many per item)
CREATE TABLE item_allergens (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
item_id uuid NOT NULL REFERENCES menu_items(id) ON DELETE CASCADE,
allergen_type text NOT NULL
);
CREATE INDEX idx_allergens_item ON item_allergens(item_id);
-- Item tags (e.g. Chef's Pick, New, Bestseller)
CREATE TABLE item_tags (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
item_id uuid NOT NULL REFERENCES menu_items(id) ON DELETE CASCADE,
tag text NOT NULL
);
CREATE INDEX idx_tags_item ON item_tags(item_id);
-- Daily specials
CREATE TABLE daily_specials (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
item_id uuid NOT NULL REFERENCES menu_items(id) ON DELETE CASCADE,
special_price numeric(8,2),
note_en text,
note_zh text,
note_ms text,
valid_date date NOT NULL DEFAULT CURRENT_DATE,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_specials_date ON daily_specials(valid_date);
-- Analytics events (public insert, admin read)
CREATE TABLE menu_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
event_type text NOT NULL,
item_id uuid REFERENCES menu_items(id) ON DELETE SET NULL,
category_id uuid REFERENCES menu_categories(id) ON DELETE SET NULL,
table_number text,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_events_type ON menu_events(event_type, created_at);
CREATE INDEX idx_events_item ON menu_events(item_id);
RLS policies:
- menu_categories, menu_items (published + available), nutrition_info, item_allergens, item_tags, daily_specials: SELECT for anon role
- menu_events: INSERT for anon, SELECT for authenticated admins only
- All write operations (INSERT/UPDATE/DELETE) on menu tables: authenticated admin only
## App Structure
src/app/
page.tsx # Redirect to /menu
menu/
page.tsx # Public digital menu (Server Component)
[category]/
page.tsx # Category anchor scroll target
admin/
layout.tsx # Admin auth guard
page.tsx # Dashboard / analytics
items/
page.tsx # Item list with availability toggles
new/page.tsx # Create item
[id]/edit/page.tsx # Edit item
categories/page.tsx # Category management
qr/page.tsx # QR code generator
api/
menu/route.ts # Public menu data endpoint
availability/route.ts # Toggle item availability
## Core Features to Build
### 1. Public Digital Menu Page (/menu)
Build a beautiful, mobile-first menu page that customers access via QR code — no login, no app download required.
- Sticky top header: restaurant name {restaurant_name}, logo, and language toggle (EN / 中文 / BM)
- Sticky horizontal category pill navigation below the header with smooth scroll-spy highlighting
- Category sections with emoji/icon headers
- Menu item cards in a responsive 1-column (mobile) / 2-column (tablet) grid
- Each card shows: photo (with lazy loading via next/image), name in selected language, price in RM, description, spicy level dots (● ● ○), Halal/Vegetarian badge, and tags (Chef's Pick, New, Bestseller)
- Tap a card to open a bottom sheet or modal with full details: larger photo, full description in all three languages, nutrition panel, allergen warnings
- Daily specials section pinned at the top with a highlighted card style
- Search bar (filters items client-side by name in all three languages)
- Filter chips: Halal Only, Vegetarian, No Peanuts, No Shellfish, No Dairy, Under RM 15
- Unavailable items shown with a grey overlay and "Sold Out" / 已售完 / Habis label — not hidden
- Log a menu_event (type: 'item_view') when a customer taps an item
### 2. Tri-lingual Support
Implement language switching with a simple React context (LanguageContext). Store preference in localStorage. On language change, all item names, descriptions, category names, and UI labels update instantly without a page reload. Support: English (EN), Chinese Simplified (中文), Bahasa Melayu (BM). Provide a complete UI strings translation object covering all labels, buttons, and messages.
### 3. Nutrition Information Panel
When a customer taps a menu item, show a clean nutrition panel inside the detail modal:
- Circular progress rings or horizontal bar chart for Calories, Protein, Carbs, Fat
- Based on % of a 2000 kcal / 50g protein / 275g carbs / 65g fat daily reference
- Serving size note (e.g., per 350g serving)
- Sodium and sugar as secondary values
- If no nutrition data exists for an item, show a subtle 'Nutrition info not available' placeholder
### 4. Allergen Warnings
Display allergen icons prominently on item cards and in the detail modal. Use standard allergen icons or emoji approximations for: Peanuts 🥜, Shellfish 🦐, Dairy 🥛, Gluten 🌾, Eggs 🥚, Soy 🫘, Tree Nuts, Sesame. Show a red-bordered warning box in the detail modal listing all allergens for that item. Add a global 'Allergen-Free Filter' in the filter chips section.
### 5. QR Code Generation (/admin/qr)
Admin page that generates QR codes:
- One QR code pointing to /menu (whole menu)
- Per-table QR codes pointing to /menu?table=1, /menu?table=2, etc.
- Download individual QR codes as PNG
- Print-ready layout: QR code + restaurant name + table number on a card template
- The table number from the URL is stored in menu_events for analytics
### 6. Admin Panel
Protect all /admin routes with Supabase Auth (email/password). Build:
**Item Management**: Table with columns: photo thumbnail, name (all 3 languages), category, price, availability toggle (real-time, calls /api/availability), published toggle, actions (edit, delete). Inline availability toggle uses optimistic UI — flips immediately, syncs to DB.
**Item Editor**: Two-column layout. Left: form with all fields (name in 3 languages, description in 3 languages, price, category select, photo upload to Supabase Storage, spicy level slider 0-3, Halal/Vegetarian checkboxes, prep time, tags input). Right: live preview of the menu card as it will appear to customers.
**Nutrition Editor**: Below the main item form, a nutrition sub-form for calories, protein, carbs, fat, sodium, sugar, serving size. Save separately or together.
**Allergen Selector**: Checkbox grid of the {allergens} allergen types for each item.
**Category Management**: Drag-to-reorder categories. Edit names in all 3 languages, assign icon/emoji, toggle active.
**Analytics Dashboard**: Cards showing — total items, available items, categories. Table of top 10 most-viewed items (from menu_events). Items flagged unavailable most often this week. Peak menu view hours (hour-by-hour bar chart using Recharts).
## Business Rules
1. Items can be toggled unavailable without deletion — unavailable items stay visible on the menu with a 'Sold Out' overlay so customers know the dish exists
2. An item must have a photo before it can be set to is_published = true — enforce this in the admin form with a clear validation message
3. All prices display in RM format (RM 12.90) — add a small SST note in the menu footer: 'Prices are SST-inclusive' / 价格已含消费税 / Harga sudah termasuk SST
4. Spicy level 0 = not spicy, 1 = mild 微辣, 2 = medium 中辣, 3 = hot 大辣 — show as filled chili icons on cards
5. Daily specials override the regular price display — show original price struck through next to the special price
6. Language preference persists in localStorage across sessions
## UI Design Direction
- Mobile-first: design for 390px wide screens first
- Color palette: white background, dark navy (#0F172A) for text and nav, yellow/gold (#FCD34D) for accents (category pills active state, CTA buttons, special badges)
- Typography: Inter for Latin text, Noto Sans SC for Chinese characters
- Category navigation: horizontally scrollable pill buttons, sticky below header
- Item cards: rounded-2xl, subtle shadow, photo takes top 55% of card, text below
- Detail modal: slides up from bottom on mobile (sheet), centered dialog on desktop
- Nutrition bars: use yellow (#FCD34D) for calories, slate for other macros
- Allergen warning: amber-50 background, amber-800 text, rounded border
- Sold Out overlay: white/80 backdrop blur over the card photo, gray badge centered
## Deliverables
1. Complete Supabase migration SQL file with all tables, indexes, and RLS policies
2. Full Next.js app with all routes listed above
3. Seed SQL with sample menu data: at least 3 categories, 8 items with photos (use placeholder.com URLs), nutrition data for 4 items, allergens for 3 items
4. README with setup steps: Supabase project setup, environment variables needed, how to run locally, how to deploy to Vercel
5. QR code generator page ready to print for tables
Begin by scaffolding the Supabase schema and the public /menu page. Get the customer-facing menu looking great on mobile before building the admin panel.