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.
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 handler3const 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 branches17 }18}1920// Streaming typewriter effect21const 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/sec24 setMessages(prev => prev.map(m =>25 m.id === currentId ? { ...m, text: text.slice(0, i), isStreaming: i < text.length } : m26 ))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 algorithm2const updateDimensions = () => {3 const mobileView = window.innerWidth < 7684 const headerSpace = 140 // NavBar (100px) + safety margins5 const availableHeight = window.innerHeight - headerSpace6 const availableWidth = window.innerWidth - (mobileView ? 20 : 80)78 let pageHeight = availableHeight9 let pageWidth = pageHeight / 1.414 // A4 aspect ratio1011 if (mobileView) {12 // Single page: clamp to available width13 if (pageWidth > availableWidth) {14 pageWidth = availableWidth15 pageHeight = pageWidth * 1.41416 }17 } else {18 // Double-page spread: two pages must fit19 if (pageWidth * 2 > availableWidth) {20 pageWidth = availableWidth / 221 pageHeight = pageWidth * 1.41422 }23 }24 setDimensions({ width: Math.floor(pageWidth), height: Math.floor(pageHeight) })25}
Canvas-based page physics with soft/hard density pages
Each flip randomises duration — feels organic, not robotic
Mobile shows single page; desktop shows two-page spread
Arrow keys flip pages; Escape closes the book overlay
Only preloads the 2 pages before and after current page
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 function2export default function cloudinaryLoader({ src, width, quality }) {3 if (!src) return '/brand/placeholder.png' // 1. Safe fallback45 if (src.includes('res.cloudinary.com')) { // 2. Full Cloudinary URL6 if (src.includes('/w_') || ...) return src // Already optimised? pass through7 const [base, file] = src.split('/upload/')8 const params = `f_auto,q_auto:best,w_${width}` // Inject transforms9 return `${base}/upload/${params}/${file}`10 }1112 if (src.startsWith('http')) return src // 3. External URL passthrough13 if (src.startsWith('/brand/')) return src // 4. Local brand assets1415 // 5. Public ID path → build full Cloudinary URL16 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.
Slower pacing than typical 3s carousels — reflects premium pace of a fine dining experience
Glassmorphism card docks left or right — alternated per outlet to create visual rhythm on scroll
Two radial gold + terra-cotta glow orbs behind the card, intensify on hover via group-hover Tailwind variant
Per-outlet social accounts — different Instagram/Facebook for Khan vs CP. Scale(1.1) hover on each icon
H2 splits outlet name word-by-word; 'OTB' is replaced with an Image fill of the logo SVG
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
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.
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.
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.
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.
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.
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.