How to fetch and display menu data from the Crave API
{
"categories": [
{
"id": "cat_123",
"name": "Appetizers",
"description": "Start your meal right",
"display_order": 1,
"is_active": true,
"products": [
{
"id": "prod_456",
"name": "Buffalo Wings",
"description": "Crispy wings with buffalo sauce",
"price": 1200,
"image_url": "https://...",
"is_available": true,
"modifiers": [
{
"id": "mod_789",
"name": "Spice Level",
"type": "radio",
"required": true,
"options": [
{
"id": "opt_101",
"name": "Mild",
"price": 0
},
{
"id": "opt_102",
"name": "Hot",
"price": 0
}
]
}
]
}
]
}
]
}
async function fetchMenu(locationId) {
try {
const response = await fetch(`https://api.cravejs.com/api/v1/locations/${locationId}/menus`, {
headers: {
'X-API-Key': process.env.CRAVE_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const menu = await response.json();
return menu;
} catch (error) {
console.error('Failed to fetch menu:', error);
throw error;
}
}
// pages/api/menu.js
export default async function handler(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { locationId } = req.query;
if (!locationId) {
return res.status(400).json({ error: 'Location ID is required' });
}
try {
const response = await fetch(`https://api.cravejs.com/api/v1/locations/${locationId}/menus`, {
headers: {
'X-API-Key': process.env.CRAVE_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
const menu = await response.json();
res.json(menu);
} catch (error) {
console.error('Menu fetch error:', error);
res.status(500).json({ error: 'Failed to fetch menu' });
}
}
import { useState, useEffect } from 'react';
export default function Menu({ locationId }) {
const [menu, setMenu] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadMenu() {
try {
setLoading(true);
const response = await fetch(`/api/menu?locationId=${locationId}`);
if (!response.ok) {
throw new Error('Failed to load menu');
}
const data = await response.json();
setMenu(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
if (locationId) {
loadMenu();
}
}, [locationId]);
if (loading) return <div>Loading menu...</div>;
if (error) return <div>Error: {error}</div>;
if (!menu) return <div>No menu available</div>;
return (
<div className="menu">
<h2>Menu</h2>
{menu.categories?.map(category => (
<CategorySection key={category.id} category={category} />
))}
</div>
);
}
function CategorySection({ category }) {
return (
<div className="category">
<h3>{category.name}</h3>
{category.description && (
<p className="category-description">{category.description}</p>
)}
<div className="products">
{category.products?.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
function ProductCard({ product }) {
return (
<div className="product-card">
{product.image_url && (
<img src={product.image_url} alt={product.name} />
)}
<div className="product-info">
<h4>{product.name}</h4>
<p className="description">{product.description}</p>
<p className="price">${(product.price / 100).toFixed(2)}</p>
{!product.is_available && (
<p className="unavailable">Currently unavailable</p>
)}
{product.modifiers && product.modifiers.length > 0 && (
<div className="modifiers">
{product.modifiers.map(modifier => (
<ModifierGroup key={modifier.id} modifier={modifier} />
))}
</div>
)}
</div>
</div>
);
}
function ModifierGroup({ modifier }) {
return (
<div className="modifier-group">
<h5>{modifier.name}</h5>
{modifier.required && <span className="required">*</span>}
<div className="modifier-options">
{modifier.options?.map(option => (
<label key={option.id} className="modifier-option">
<input
type={modifier.type === 'radio' ? 'radio' : 'checkbox'}
name={modifier.id}
value={option.id}
/>
<span>{option.name}</span>
{option.price > 0 && (
<span className="option-price">
+${(option.price / 100).toFixed(2)}
</span>
)}
</label>
))}
</div>
</div>
);
}
function useMenuFilters(menu) {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const filteredMenu = useMemo(() => {
if (!menu) return null;
return {
...menu,
categories: menu.categories
.filter(category =>
!selectedCategory || category.id === selectedCategory
)
.map(category => ({
...category,
products: category.products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
)
}))
.filter(category => category.products.length > 0)
};
}, [menu, searchTerm, selectedCategory]);
return {
filteredMenu,
searchTerm,
setSearchTerm,
selectedCategory,
setSelectedCategory
};
}
const menuCache = new Map();
async function getCachedMenu(locationId) {
const cacheKey = `menu_${locationId}`;
// Check cache first
if (menuCache.has(cacheKey)) {
const cached = menuCache.get(cacheKey);
// Check if cache is still valid (5 minutes)
if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data;
}
}
// Fetch fresh data
const menu = await fetchMenu(locationId);
// Cache the result
menuCache.set(cacheKey, {
data: menu,
timestamp: Date.now()
});
return menu;
}
function handleMenuError(error) {
switch (error.status) {
case 401:
return 'Invalid API key. Please check your configuration.';
case 403:
return 'Access forbidden. Merchant subscription may be required.';
case 404:
return 'Menu not found. Please check the location ID.';
case 429:
return 'Too many requests. Please wait before trying again.';
default:
return 'Failed to load menu. Please try again.';
}
}
function MenuWithStates({ locationId }) {
const [menu, setMenu] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const loadMenu = async () => {
setLoading(true);
setError(null);
try {
const data = await fetchMenu(locationId);
setMenu(data);
} catch (err) {
setError(handleMenuError(err));
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="menu-loading">
<div className="spinner"></div>
<p>Loading menu...</p>
</div>
);
}
if (error) {
return (
<div className="menu-error">
<p>{error}</p>
<button onClick={loadMenu}>Retry</button>
</div>
);
}
return <Menu menu={menu} />;
}
.menu {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.category {
margin-bottom: 3rem;
}
.category h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #333;
}
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
background: white;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 1rem;
}
.price {
font-size: 1.2rem;
font-weight: bold;
color: #007bff;
}
.unavailable {
color: #dc3545;
font-style: italic;
}
.modifier-group {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #eee;
}
.modifier-option {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.option-price {
margin-left: auto;
color: #666;
}