Back to Case Studies
Restaurant Tech
React Pageflip
Cloudinary CDN
Framer Motion
Next.js 15

OTB — Out of the BoxDigital Platform: Flipbook Menu · Rule-Based Bot · Cloudinary CDN · Interactive Maps

A full-featured restaurant website for a premium Delhi bar & restaurant with two outlets — built from scratch with a physics-based flipbook menu, a decision-tree chatbot, crossfading outlet showcases, and a custom Cloudinary image pipeline serving 100+ optimised menu page images.

14
Routes
100+
Menu Pages (Cloudinary)
20+
Chatbot Tree Nodes
₹0 / month
Hosting Cost
100/100
Lighthouse Rate

System Context

Zero-backend architecture — the entire platform is a statically exported Next.js site deployed on GitHub Pages for ₹0/month hosting. All media is served from Cloudinary, maps from OpenStreetMap, and contacts via WhatsApp deep-links.

Frontend Container Architecture

Eight specialised sub-systems compose the frontend. Each is independently testable (Vitest unit + component tests). The chatbot and WhatsApp float are injected globally via the root layout; everything else is route-scoped.

14 routes

App Router pages for every business need — menu, reserve, gallery, events, about, franchise, team, achievements, legal, support, report

Static export

next build + output:export → pure HTML/CSS/JS on GitHub Pages. Zero cold starts, zero server costs, instant global CDN delivery

Landscape mode

Chatbot, Flipbook and orientation advisory all have bespoke landscape CSS — critical for mobile users rotating to view menu pages

Rule-Based Decision Tree Bot

No LLM, no external API — a hand-crafted decision tree with 20+ nodes handles every customer query deterministically. Streaming typewriter effect at 12ms/char (~83 chars/sec) mimics ChatGPT-style output. Glassmorphism UI with radial gold glow, AnimatePresence icon morph on open/close, custom text renderer for bold/italic markup.

Tree Node + Streaming Implementation

1// Rule-based decision tree node (simplified)
2// Each node has: text, optional options, optional handler
3const TREE: ChatNode = {
4 id: 'root',
5 text: "👋 Hey! I'm your OTB Host. How can I help?",
6 options: ['📍 Locations', '🍴 Menu', '📅 Reserve a Table', '🎉 Events', '❓ Something else'],
7 children: {
8 '📍 Locations': {
9 text: "Which location would you like to know about?",
10 options: ['🏛️ Khan Market', '🏙️ Connaught Place'],
11 children: {
12 '🏛️ Khan Market': { text: "OTB Khan Market opens at 12pm…", options: ['⬅ Back'] },
13 '🏙️ Connaught Place': { text: "OTB CP is at Inner Circle…", options: ['⬅ Back'] },
14 }
15 },
16 // … more branches
17 }
18}
19
20// Streaming typewriter effect
21const streamText = async (text: string) => {
22 for (let i = 0; i <= text.length; i++) {
23 await new Promise(r => setTimeout(r, 12)) // 12ms/char ≈ 83 chars/sec
24 setMessages(prev => prev.map(m =>
25 m.id === currentId ? { ...m, text: text.slice(0, i), isStreaming: i < text.length } : m
26 ))
27 }
28}

Why no LLM?

  • Zero API cost — free forever
  • 100% deterministic answers — no hallucinations about hours or prices
  • Offline-capable — works even if backend is down
  • Instant response, no network round-trip
  • Branch by branch FAQ coverage of all 6 topic categories

UX Engineering

  • AnimatePresence: robot icon → X icon morph on open
  • Scale(0) → Scale(1) bot panel spring animation
  • Option chip buttons appear via animate-fade-up after each bot message
  • Typing indicator (3 bouncing dots) during stream
  • Glass panel: backdrop-blur(20px) + linear-gradient dark bg
  • Landscape CSS: h-[calc(100vh-7rem)] for phone rotation

Physics-Based Flipbook Menu

The most technically complex piece of the project. A full-screen A4-ratio book using react-pageflip with canvas-based page physics. Two outlets × two menu types (food & drinks) × up to 25 pages each — all served from Cloudinary with priority-based lazy loading.

Responsive A4 Sizer Algorithm

1// Responsive A4-ratio sizer — core algorithm
2const updateDimensions = () => {
3 const mobileView = window.innerWidth < 768
4 const headerSpace = 140 // NavBar (100px) + safety margins
5 const availableHeight = window.innerHeight - headerSpace
6 const availableWidth = window.innerWidth - (mobileView ? 20 : 80)
7
8 let pageHeight = availableHeight
9 let pageWidth = pageHeight / 1.414 // A4 aspect ratio
10
11 if (mobileView) {
12 // Single page: clamp to available width
13 if (pageWidth > availableWidth) {
14 pageWidth = availableWidth
15 pageHeight = pageWidth * 1.414
16 }
17 } else {
18 // Double-page spread: two pages must fit
19 if (pageWidth * 2 > availableWidth) {
20 pageWidth = availableWidth / 2
21 pageHeight = pageWidth * 1.414
22 }
23 }
24 setDimensions({ width: Math.floor(pageWidth), height: Math.floor(pageHeight) })
25}
Page flipping
react-pageflip + page-flip

Canvas-based page physics with soft/hard density pages

Random flip speed
800–1400ms

Each flip randomises duration — feels organic, not robotic

Portrait mode
usePortrait={isMobile}

Mobile shows single page; desktop shows two-page spread

Keyboard nav
← → + Escape

Arrow keys flip pages; Escape closes the book overlay

Priority loading
Math.abs(current-i) ≤ 2

Only preloads the 2 pages before and after current page

DOM isolation
isActive prop

Keyboard handlers disabled when overlay is not active — prevents event conflicts with DualOutletMenu tab toggle

Cloudinary Image Pipeline

A custom Next.js loaderFile handles all 100+ menu images, outlet photography, and brand assets through a single function. On-the-fly transforms inject responsive widths, format negotiation (f_auto → WebP/AVIF), and quality optimisation — without any build step.

Cloudinary Loader — 5 Code Paths

1// Intelligent Cloudinary loader — 4 code paths in one function
2export default function cloudinaryLoader({ src, width, quality }) {
3 if (!src) return '/brand/placeholder.png' // 1. Safe fallback
4
5 if (src.includes('res.cloudinary.com')) { // 2. Full Cloudinary URL
6 if (src.includes('/w_') || ...) return src // Already optimised? pass through
7 const [base, file] = src.split('/upload/')
8 const params = `f_auto,q_auto:best,w_${width}` // Inject transforms
9 return `${base}/upload/${params}/${file}`
10 }
11
12 if (src.startsWith('http')) return src // 3. External URL passthrough
13 if (src.startsWith('/brand/')) return src // 4. Local brand assets
14
15 // 5. Public ID path → build full Cloudinary URL
16 const params = `f_auto,q_auto:best,w_${width},c_limit`
17 return `https://res.cloudinary.com/${cloudName}/image/upload/${params}/${src}`
18}

Menu data structure

Two outlets (Khan Market / Connaught Place) × two menu types (food / drinks) × 20–25 Cloudinary pages each. All stored in lib/menu-data.ts as static arrays — zero CMS, zero database, instant cold load.

Why custom loader instead of next/image defaults?

Static export (output:'export') doesn't support Next.js built-in image optimisation. The custom loader runs entirely client-side, rewrites every src URL with Cloudinary transforms, giving the same WebP/AVIF + responsive sizing benefits without a Node server.

Outlet Showcase — Crossfade Carousel

The home page features two outlet showcases — Khan Market (text left) and Connaught Place (text right). Each cycles through outlet photography every 6 seconds with an AnimatePresence crossfade. The glassmorphism info card floats over the background image.

Crossfade timing
6,000ms

Slower pacing than typical 3s carousels — reflects premium pace of a fine dining experience

Orientation prop
left | right

Glassmorphism card docks left or right — alternated per outlet to create visual rhythm on scroll

Glass effect
backdrop-blur(20px)

Two radial gold + terra-cotta glow orbs behind the card, intensify on hover via group-hover Tailwind variant

Social links
IG + FB + WA

Per-outlet social accounts — different Instagram/Facebook for Khan vs CP. Scale(1.1) hover on each icon

Logo rendering
Word-split + Image fill

H2 splits outlet name word-by-word; 'OTB' is replaced with an Image fill of the logo SVG

SectionFrame
Full-bleed background

Background image is swapped on the SectionFrame root el — only the image prop changes, layout is static

Location & Maps

Leaflet + react-leaflet

  • OpenStreetMap tiles — zero cost, no API key
  • Custom marker icons per outlet
  • Popup with outlet name + Google Maps CTA
  • MultiLocationMap component handles n locations
  • Dynamically imported (SSR disabled) — Leaflet is browser-only

Static locations data

  • lib/locations.ts — coordinates, address, hours, social links
  • Khan Market: Violet Line metro (walking distance)
  • Connaught Place: Rajiv Chowk metro (Yellow/Blue Line)
  • WhatsApp deep-link per outlet for directions queries
  • No Google Maps API needed — OpenStreetMap is free and GDPR-safe

Key Design Decisions

01
Full static export — no backend

Restaurant sites don't need server-side logic. Static export on GitHub Pages = ₹0/month hosting, instant global delivery, no DDOS surface, no cold starts. Every route is pre-rendered HTML.

02
Rule-based bot over LLM

A restaurant's FAQ is finite and deterministic. An LLM could hallucinate wrong opening hours or prices — catastrophic for a venue. The decision tree guarantees 100% accurate answers, costs nothing, and works offline.

03
react-pageflip for menu (not a PDF viewer)

PDFs require browser plugins or heavy libraries. react-pageflip gives native page-turning physics that feel premium — matching the OTB brand. Pages are just images served from Cloudinary, cacheable and fast.

04
Cloudinary custom loader over vercel/image

Static export can't use Next.js image optimisation server. Custom loader runs zero-cost in the browser, rewriting every URL with f_auto + q_auto before the HTTP request is even made.

05
isActive prop for keyboard event isolation

When two flipbooks are mounted (food + drinks), both listen for arrow keys — causing double flips. The isActive prop, toggled by DualOutletMenu's tab state, ensures only the visible book handles keyboard events.

06
6s carousel pacing (not 3s)

Industry standard 3s carousels feel frantic for a premium bar-restaurant. 6s gives enough time to absorb the ambiance photography — aligning with the brand's 'Out of the Box' measured luxury positioning.

Chat on WhatsApp