Different modal dialogs for:
The API data is a JSON response from the endpoint "https://www.foodchow.com/api/FoodChowWD/GetRestaurantMenuWDWidget_multi" with ShopId=3161. The data contains:
Menu items with details like:
I need to create a React application that integrates with this API to recreate a similar food ordering experience. I'll use Next.js with the App Router to build this application, focusing on:
I'll use Tailwind CSS for styling and shadcn/ui components where appropriate to match the design shown in the screenshots.
</Thinking>
I'll create a React application that integrates with the FoodChow API to build a restaurant menu and ordering system similar to the one shown in your screenshots.
<tsx file="app/page.tsx">
import { useState, useEffect } from 'react';
import RestaurantHeader from '@/components/restaurant-header';
import MenuCategories from '@/components/menu-categories';
import MenuItems from '@/components/menu-items';
import Cart from '@/components/cart';
import SearchBar from '@/components/search-bar';
import { fetchMenuData } from '@/lib/api';
export default function Home() { const [menuData, setMenuData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedCategory, setSelectedCategory] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [cart, setCart] = useState([]);
useEffect(() => { const loadMenuData = async () => { try { setLoading(true); const data = await fetchMenuData(); setMenuData(data); if (data?.CategoryList?.length > 0) { setSelectedCategory(data.CategoryList[0].CategryId); } } catch (err) { setError('Failed to load menu data'); console.error(err); } finally { setLoading(false); } };
loadMenuData();
}, []);
const addToCart = (item) => { setCart(prevCart => { const existingItem = prevCart.find(cartItem => cartItem.ItemId === item.ItemId && cartItem.SizeId === item.SizeId );
if (existingItem) {
return prevCart.map(cartItem =>
cartItem.ItemId === item.ItemId && cartItem.SizeId === item.SizeId
? { ...cartItem, quantity: cartItem.quantity + 1 }
: cartItem
);
} else {
return [...prevCart, { ...item, quantity: 1 }];
}
});
};
const handleSearch = (query) => { setSearchQuery(query); };
const filteredItems = selectedCategory && menuData?.CategoryList ? menuData.CategoryList.find(cat => cat.CategryId === selectedCategory)?.ItemListWidget.filter( item => item.ItemName.toLowerCase().includes(searchQuery.toLowerCase()) ) : [];
if (loading) return <div className="flex justify-center items-center min-h-screen">
Loading...</div>
;
if (error) return <div className="flex justify-center items-center min-h-screen text-red-500">
{error}</div>
;
if (!menuData) return <div className="flex justify-center items-center min-h-screen">
No menu data available</div>
;
return (
<main className="min-h-screen bg-white">
<RestaurantHeader restaurantInfo={menuData} />
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 p-4">
<div className="md:col-span-3">
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-xl font-bold text-blue-600 mb-4">CATEGORIES</h2>
<MenuCategories
categories={menuData.CategoryList}
selectedCategory={selectedCategory}
onSelectCategory={setSelectedCategory}
/>
</div>
</div>
<div className="md:col-span-6">
<SearchBar onSearch={handleSearch} />
<div className="bg-white rounded-lg shadow mt-4">
{selectedCategory && (
<MenuItems
items={filteredItems}
onAddToCart={addToCart}
categoryName={menuData.CategoryList.find(cat => cat.CategryId === selectedCategory)?.CategryName}
/>
)}
</div>
</div>
<div className="md:col-span-3">
<div className="bg-white rounded-lg shadow p-4">
<Cart cart={cart} setCart={setCart} />
</div>
</div>
</div>
</main>
);
}
</tsx>
<tsx file="components/restaurant-header.tsx">
import { Button } from "@/components/ui/button";
import { Phone } from 'lucide-react';
export default function RestaurantHeader({ restaurantInfo }) {
return (
<div className="bg-white border-b p-4">
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center gap-4">
<img
src="/placeholder.svg?height=80&width=80"
alt="Restaurant Logo"
className="w-16 h-16 rounded-full"
/>
<div>
<h1 className="text-2xl font-bold text-gray-800">
FoodChow Demo India</h1>
<p className="text-gray-600">
Valsad, Gujarat, India</p>
<div className="flex items-center mt-1">
<span className="text-green-600 font-medium">
Restaurant Is Open</span>
<span className="mx-2">
•</span>
<span className="text-gray-600">
Timing 07:00 AM - 11:00 PM</span>
</div>
</div>
</div>
<div className="flex gap-2 mt-4 md:mt-0">
<Button variant="outline" className="border-blue-500 text-blue-500 hover:bg-blue-50">
Choose Service
</Button>
<Button variant="outline" className="border-blue-500 text-blue-500 hover:bg-blue-50">
Book Now
</Button>
<Button className="bg-blue-500 hover:bg-blue-600 text-white rounded-full">
<Phone className="mr-2 h-4 w-4" />
7016997342
</Button>
</div>
</div>
<div className="container mx-auto mt-4 flex gap-2">
<Button className="bg-blue-500 hover:bg-blue-600 text-white rounded-full">
Main Menu
</Button>
<Button variant="outline" className="border-blue-500 text-blue-500 hover:bg-blue-50 rounded-full">
Breakfast
</Button>
</div>
</div>
);
}
</tsx>
<tsx file="components/menu-categories.tsx">
import { cn } from "@/lib/utils";
export default function MenuCategories({ categories, selectedCategory, onSelectCategory }) {
return (
<div className="flex flex-col">
{categories.map((category) => (
<button
key={category.CategryId}
className={cn(
"text-left p-3 hover:bg-blue-50 transition-colors",
selectedCategory === category.CategryId ? "bg-blue-500 text-white" : "text-gray-700"
)}
onClick={() => onSelectCategory(category.CategryId)}
>
{category.CategryName}
</button>
))}
</div>
);
}
</tsx>
<tsx file="components/menu-items.tsx">
import { Button } from "@/components/ui/button";
export default function MenuItems({ items, onAddToCart, categoryName }) {
if (!items || items.length === 0) {
return (
<div className="p-6 text-center text-gray-500">
No items found in this category
</div>
);
}
return (
<div>
<div className="p-4 border-b">
<h2 className="text-xl font-bold">
{categoryName}</h2>
<p className="text-sm text-gray-500">
{items.length} items</p>
</div>
<div className="divide-y">
{items.map((item) => (
<div key={item.ItemId} className="p-4 flex justify-between items-center">
<div>
<h3 className="font-medium text-gray-800">{item.ItemName}</h3>
{item.Description && (
<p className="text-sm text-gray-500 mt-1">{item.Description}</p>
)}
<div className="mt-1 text-blue-600 font-medium">
{item.IsSizeAvailable === 1 && item.SizeListWidget?.length > 0 ? (
<span>Rs. {item.SizeListWidget[0].Price}</span>
) : (
<span>Rs. {item.Price}</span>
)}
</div>
</div>
<Button
onClick={() => onAddToCart(item)}
className="bg-white text-blue-500 border border-blue-500 hover:bg-blue-50"
>
ADD
</Button>
</div>
))}
</div>
</div>
);
}
</tsx>
<tsx file="components/cart.tsx">
import { Button } from "@/components/ui/button";
import { Trash2 } from 'lucide-react';
export default function Cart({ cart, setCart }) { const removeFromCart = (itemId, sizeId) => { setCart(prevCart => prevCart.filter(item => !(item.ItemId === itemId && item.SizeId === sizeId) )); };
const updateQuantity = (itemId, sizeId, newQuantity) => { if (newQuantity <= 0) { removeFromCart(itemId, sizeId); return; }
setCart(prevCart => prevCart.map(item =>
item.ItemId === itemId && item.SizeId === sizeId
? { ...item, quantity: newQuantity }
: item
));
};
const calculateTotal = () => { return cart.reduce((total, item) => { const price = item.IsSizeAvailable === 1 && item.SizeListWidget?.length > 0 ? item.SizeListWidget[0].Price : item.Price; return total + (price * item.quantity); }, 0); };
return (
<div>
<h2 className="text-xl font-bold text-blue-600 mb-4">
Your Cart</h2>
{cart.length === 0 ? (
<div className="text-center py-8">
<div className="flex justify-center mb-4">
<img
src="/placeholder.svg?height=150&width=150"
alt="Empty Cart"
className="w-32 h-32 opacity-70"
/>
</div>
<p className="text-gray-500 mb-2">Your Cart Is Empty!</p>
<p className="text-gray-500">Add Some Delicious Food Items And Satisfy Your Cravings.</p>
<span className="text-yellow-500 text-2xl mt-2 inline-block">😋</span>
</div>
) : (
<div>
<div className="space-y-4 mb-4">
{cart.map((item) => {
const price = item.IsSizeAvailable === 1 && item.SizeListWidget?.length > 0
? item.SizeListWidget[0].Price
: item.Price;
return (
<div key={`${item.ItemId}-${item.SizeId}`} className="flex justify-between items-center border-b pb-2">
<div>
<h3 className="font-medium">{item.ItemName}</h3>
<p className="text-sm text-gray-500">Rs. {price} x {item.quantity}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => updateQuantity(item.ItemId, item.SizeId, item.quantity - 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 rounded-full"
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => updateQuantity(item.ItemId, item.SizeId, item.quantity + 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 rounded-full"
>
+
</button>
<button
onClick={() => removeFromCart(item.ItemId, item.SizeId)}
className="text-red-500 ml-2"
>
<Trash2 size={16} />
</button>
</div>
</div>
);
})}
</div>
<div className="mt-4">
<div className="flex justify-between font-bold mb-4">
<span>Total:</span>
<span>Rs. {calculateTotal()}</span>
</div>
<Button className="w-full bg-blue-500 hover:bg-blue-600">
Checkout
</Button>
</div>
</div>
)}
</div>
);
}
</tsx>
<tsx file="components/search-bar.tsx">
import { useState } from 'react';
import { Search } from 'lucide-react';
export default function SearchBar({ onSearch }) { const [query, setQuery] = useState('');
const handleSubmit = (e) => { e.preventDefault(); onSearch(query); };
return (
<form onSubmit={handleSubmit} className="relative">
<input
type="text"
placeholder="Search for dishes"
className="w-full p-3 pr-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button
type="submit"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-blue-500"
>
<Search size={20} />
</button>
</form>
);
}
</tsx>
<tsx file="components/service-modal.tsx">
import { useState } from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
export default function ServiceModal({ isOpen, onClose }) { const [selectedService, setSelectedService] = useState(null);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full max-w-md">
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-xl font-bold">
Choose A Service</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
<X size={24} />
</button>
</div>
<div className="p-6 grid grid-cols-3 gap-4">
<ServiceOption
icon={<img src="/placeholder.svg?height=50&width=50" alt="Pickup" className="w-12 h-12 mx-auto" />}
name="Pickup"
selected={selectedService === 'pickup'}
onClick={() => setSelectedService('pickup')}
/>
<ServiceOption
icon={<img src="/placeholder.svg?height=50&width=50" alt="Dine-In" className="w-12 h-12 mx-auto" />}
name="Dine-In"
selected={selectedService === 'dine-in'}
onClick={() => setSelectedService('dine-in')}
/>
<ServiceOption
icon={<img src="/placeholder.svg?height=50&width=50" alt="Delivery" className="w-12 h-12 mx-auto" />}
name="Delivery"
selected={selectedService === 'delivery'}
onClick={() => setSelectedService('delivery')}
/>
</div>
<div className="p-4 border-t">
<Button
className="w-full bg-blue-500 hover:bg-blue-600"
disabled={!selectedService}
onClick={() => {
// Handle service selection
onClose();
}}
>
Proceed
</Button>
</div>
</div>
</div>
); }
function ServiceOption({ icon, name, selected, onClick }) {
return (
<button
className={p-4 rounded-lg border ${
selected ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
} flex flex-col items-center justify-center transition-colors
}
onClick={onClick}
>
{icon}
<span className="mt-2 font-medium">
{name}</span>
</button>
);
}
</tsx>
<tsx file="lib/api.js">
export async function fetchMenuData() {
try {
const response = await fetch('https://www.foodchow.com/api/FoodChowWD/GetRestaurantMenuWDWidget_multi?ShopId=3161&locale_id=null');
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
if (data.response_code === "1") {
// Parse the data string into a JavaScript object
return JSON.parse(data.data);
} else {
throw new Error(data.message || 'Failed to fetch menu data');
}
} catch (error) {
console.error('Error fetching menu data:', error);
throw error;
}
}
</tsx>
<tsx file="app/layout.tsx">
import { Inter } from 'next/font/google'
import './globals.css'
import { ThemeProvider } from "@/components/theme-provider"
const inter = Inter({ subsets: ['latin'] })
export const metadata = { title: 'FoodChow Demo - Food Ordering App', description: 'Order delicious food online from FoodChow Demo', }
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
</tsx>
<tsx file="tailwind.config.js">
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "#3B82F6", // blue-500
foreground: "#FFFFFF",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
</tsx>
This food ordering application includes the following features based on the FoodChow API:
To enhance this application further, you could:
You can deploy this application to Vercel by clicking the "Deploy" button in the top right corner of the UI.