Real-world implementations and practical examples using the Crave.js API
import { useState, useEffect } from 'react';
import { useCart } from '../hooks/useCart';
const MenuPage = ({ locationSlug }) => {
const [menu, setMenu] = useState([]);
const [loading, setLoading] = useState(true);
const { addToCart, isLoading: cartLoading } = useCart();
useEffect(() => {
fetchMenu();
}, [locationSlug]);
const fetchMenu = async () => {
try {
const response = await fetch(`/api/locations/${locationSlug}/menu`);
const data = await response.json();
setMenu(data.items || []);
} catch (error) {
console.error('Failed to fetch menu:', error);
} finally {
setLoading(false);
}
};
const handleAddToCart = async (product) => {
try {
await addToCart(product.id, 1);
} catch (error) {
console.error('Failed to add to cart:', error);
}
};
if (loading) return <div>Loading menu...</div>;
return (
<div className="menu-page">
<h1>Our Menu</h1>
<div className="menu-grid">
{menu.map(item => (
<div key={item.id} className="menu-item">
<img src={item.image} alt={item.name} />
<div className="item-info">
<h3>{item.name}</h3>
<p>{item.description}</p>
<div className="item-footer">
<span className="price">${item.price}</span>
<button
onClick={() => handleAddToCart(item)}
disabled={cartLoading || !item.available}
>
{item.available ? 'Add to Cart' : 'Unavailable'}
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default MenuPage;
// services/LocationService.js
class LocationService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = process.env.CRAVE_API_BASE;
}
async getAllLocations() {
const response = await fetch(`${this.baseUrl}/admin/locations`, {
headers: { 'X-API-Key': this.apiKey }
});
return response.json();
}
async getLocationBySlug(slug) {
const response = await fetch(`${this.baseUrl}/locations/${slug}`, {
headers: { 'X-API-Key': this.apiKey }
});
return response.json();
}
async updateLocationHours(locationId, hours) {
const response = await fetch(`${this.baseUrl}/admin/locations/${locationId}/hours`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey
},
body: JSON.stringify({ hours })
});
return response.json();
}
async getLocationAnalytics(locationId, startDate, endDate) {
const params = new URLSearchParams({
startDate: startDate.toISOString(),
endDate: endDate.toISOString()
});
const response = await fetch(`${this.baseUrl}/admin/locations/${locationId}/analytics?${params}`, {
headers: { 'X-API-Key': this.apiKey }
});
return response.json();
}
}
// Usage in a React component
const LocationDashboard = () => {
const [locations, setLocations] = useState([]);
const [selectedLocation, setSelectedLocation] = useState(null);
const [analytics, setAnalytics] = useState(null);
const locationService = new LocationService(process.env.NEXT_PUBLIC_CRAVE_API_KEY);
useEffect(() => {
loadLocations();
}, []);
const loadLocations = async () => {
try {
const data = await locationService.getAllLocations();
setLocations(data.locations || []);
} catch (error) {
console.error('Failed to load locations:', error);
}
};
const loadAnalytics = async (locationId) => {
try {
const endDate = new Date();
const startDate = new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
const data = await locationService.getLocationAnalytics(locationId, startDate, endDate);
setAnalytics(data);
} catch (error) {
console.error('Failed to load analytics:', error);
}
};
return (
<div className="location-dashboard">
<div className="locations-list">
<h2>Restaurant Locations</h2>
{locations.map(location => (
<div
key={location.id}
className={`location-card ${selectedLocation?.id === location.id ? 'selected' : ''}`}
onClick={() => {
setSelectedLocation(location);
loadAnalytics(location.id);
}}
>
<h3>{location.name}</h3>
<p>{location.address}</p>
<div className="location-stats">
<span className={`status ${location.status}`}>
{location.status}
</span>
<span className="orders-today">
{location.todayOrders || 0} orders today
</span>
</div>
</div>
))}
</div>
{selectedLocation && (
<div className="location-details">
<h2>{selectedLocation.name}</h2>
{analytics && (
<div className="analytics-grid">
<div className="metric">
<h3>Total Orders</h3>
<p>{analytics.totalOrders}</p>
</div>
<div className="metric">
<h3>Revenue</h3>
<p>${analytics.totalRevenue}</p>
</div>
<div className="metric">
<h3>Average Order</h3>
<p>${analytics.averageOrderValue}</p>
</div>
<div className="metric">
<h3>Popular Items</h3>
<ul>
{analytics.popularItems?.map(item => (
<li key={item.id}>{item.name} ({item.count})</li>
))}
</ul>
</div>
</div>
)}
</div>
)}
</div>
);
};
// services/OrderService.js
class OrderService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = process.env.CRAVE_API_BASE;
}
async getOrders(locationId, status = 'all', page = 1, limit = 20) {
const params = new URLSearchParams({
status,
page: page.toString(),
limit: limit.toString()
});
const response = await fetch(`${this.baseUrl}/admin/locations/${locationId}/orders?${params}`, {
headers: { 'X-API-Key': this.apiKey }
});
return response.json();
}
async updateOrderStatus(orderId, status, estimatedTime = null) {
const body = { status };
if (estimatedTime) body.estimatedTime = estimatedTime;
const response = await fetch(`${this.baseUrl}/admin/orders/${orderId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey
},
body: JSON.stringify(body)
});
return response.json();
}
async getOrderDetails(orderId) {
const response = await fetch(`${this.baseUrl}/admin/orders/${orderId}`, {
headers: { 'X-API-Key': this.apiKey }
});
return response.json();
}
// WebSocket connection for real-time updates
connectToOrderUpdates(locationId, onOrderUpdate) {
const ws = new WebSocket(`wss://api.yourrestaurant.com/ws/orders/${locationId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
onOrderUpdate(data);
};
ws.onopen = () => {
console.log('Connected to order updates');
};
ws.onclose = () => {
console.log('Disconnected from order updates');
// Reconnect logic
setTimeout(() => this.connectToOrderUpdates(locationId, onOrderUpdate), 5000);
};
return ws;
}
}
// React component for kitchen display
const KitchenDisplay = ({ locationId }) => {
const [orders, setOrders] = useState([]);
const [selectedOrder, setSelectedOrder] = useState(null);
const orderService = new OrderService(process.env.CRAVE_API_KEY);
useEffect(() => {
loadOrders();
// Connect to real-time updates
const ws = orderService.connectToOrderUpdates(locationId, (orderUpdate) => {
setOrders(prevOrders => {
const updatedOrders = prevOrders.map(order =>
order.id === orderUpdate.id ? { ...order, ...orderUpdate } : order
);
// Add new orders
if (!prevOrders.find(o => o.id === orderUpdate.id)) {
updatedOrders.push(orderUpdate);
}
return updatedOrders.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
});
});
return () => ws.close();
}, [locationId]);
const loadOrders = async () => {
try {
const data = await orderService.getOrders(locationId, 'pending');
setOrders(data.orders || []);
} catch (error) {
console.error('Failed to load orders:', error);
}
};
const updateOrderStatus = async (orderId, newStatus, estimatedTime) => {
try {
await orderService.updateOrderStatus(orderId, newStatus, estimatedTime);
// Update will come through WebSocket
} catch (error) {
console.error('Failed to update order status:', error);
}
};
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
preparing: 'blue',
ready: 'green',
completed: 'gray',
cancelled: 'red'
};
return colors[status] || 'gray';
};
return (
<div className="kitchen-display">
<h1>Kitchen Display - {orders.length} Active Orders</h1>
<div className="orders-grid">
{orders.map(order => (
<div
key={order.id}
className={`order-card ${order.status}`}
onClick={() => setSelectedOrder(order)}
>
<div className="order-header">
<span className="order-number">#{order.orderNumber}</span>
<span className={`status ${getStatusColor(order.status)}`}>
{order.status}
</span>
</div>
<div className="order-time">
<span>Placed: {new Date(order.createdAt).toLocaleTimeString()}</span>
{order.estimatedTime && (
<span>ETA: {new Date(order.estimatedTime).toLocaleTimeString()}</span>
)}
</div>
<div className="order-items">
{order.items.map(item => (
<div key={item.id} className="item">
<span className="quantity">{item.quantity}x</span>
<span className="name">{item.name}</span>
{item.modifiers && item.modifiers.length > 0 && (
<div className="modifiers">
{item.modifiers.map(mod => (
<span key={mod.id} className="modifier">+{mod.name}</span>
))}
</div>
)}
</div>
))}
</div>
<div className="order-actions">
{order.status === 'pending' && (
<button
onClick={(e) => {
e.stopPropagation();
updateOrderStatus(order.id, 'preparing', new Date(Date.now() + 15 * 60 * 1000));
}}
className="btn-start"
>
Start Preparing
</button>
)}
{order.status === 'preparing' && (
<button
onClick={(e) => {
e.stopPropagation();
updateOrderStatus(order.id, 'ready');
}}
className="btn-ready"
>
Mark Ready
</button>
)}
{order.status === 'ready' && (
<button
onClick={(e) => {
e.stopPropagation();
updateOrderStatus(order.id, 'completed');
}}
className="btn-complete"
>
Complete
</button>
)}
</div>
</div>
))}
</div>
{selectedOrder && (
<OrderDetailsModal
order={selectedOrder}
onClose={() => setSelectedOrder(null)}
onUpdateStatus={updateOrderStatus}
/>
)}
</div>
);
};
// services/PaymentService.js
class PaymentService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = process.env.CRAVE_API_BASE;
}
async createPaymentIntent(cartId, amount, currency = 'usd') {
const response = await fetch(`${this.baseUrl}/payments/create-intent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey
},
body: JSON.stringify({
cartId,
amount: Math.round(amount * 100), // Convert to cents
currency
})
});
if (!response.ok) throw new Error('Failed to create payment intent');
return response.json();
}
async confirmPayment(paymentIntentId, paymentMethodId) {
const response = await fetch(`${this.baseUrl}/payments/confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey
},
body: JSON.stringify({
paymentIntentId,
paymentMethodId
})
});
if (!response.ok) throw new Error('Failed to confirm payment');
return response.json();
}
async processRefund(orderId, amount, reason) {
const response = await fetch(`${this.baseUrl}/payments/refund`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey
},
body: JSON.stringify({
orderId,
amount: Math.round(amount * 100),
reason
})
});
if (!response.ok) throw new Error('Failed to process refund');
return response.json();
}
}
// React checkout component
const CheckoutForm = ({ cart, onPaymentSuccess }) => {
const [stripe, setStripe] = useState(null);
const [elements, setElements] = useState(null);
const [clientSecret, setClientSecret] = useState('');
const [processing, setProcessing] = useState(false);
const [error, setError] = useState('');
const paymentService = new PaymentService(process.env.NEXT_PUBLIC_CRAVE_API_KEY);
useEffect(() => {
initializeStripe();
createPaymentIntent();
}, []);
const initializeStripe = async () => {
const stripeInstance = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
setStripe(stripeInstance);
};
const createPaymentIntent = async () => {
try {
const { clientSecret } = await paymentService.createPaymentIntent(
cart.id,
cart.total
);
setClientSecret(clientSecret);
} catch (error) {
setError('Failed to initialize payment');
}
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
setError('');
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: event.target.name.value,
email: event.target.email.value
}
}
});
if (error) {
setError(error.message);
setProcessing(false);
} else {
onPaymentSuccess(paymentIntent);
}
};
return (
<form onSubmit={handleSubmit} className="checkout-form">
<div className="customer-info">
<input name="name" placeholder="Full Name" required />
<input name="email" type="email" placeholder="Email" required />
<input name="phone" type="tel" placeholder="Phone" required />
</div>
<div className="payment-section">
<h3>Payment Information</h3>
<Elements stripe={stripe}>
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
},
}}
/>
</Elements>
</div>
<div className="order-summary">
<h3>Order Summary</h3>
{cart.items.map(item => (
<div key={item.id} className="summary-item">
<span>{item.quantity}x {item.name}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
<div className="total">
<strong>Total: ${cart.total.toFixed(2)}</strong>
</div>
</div>
{error && <div className="error-message">{error}</div>}
<button
type="submit"
disabled={!stripe || processing}
className="pay-button"
>
{processing ? 'Processing...' : `Pay $${cart.total.toFixed(2)}`}
</button>
</form>
);
};
// services/CraveApiClient.js
class CraveApiClient {
constructor(apiKey, baseUrl) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
...options,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers
}
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API Request failed:', error);
throw error;
}
}
// Location methods
async getLocations() {
return this.request('/locations');
}
async getLocationMenu(locationSlug) {
return this.request(`/locations/${locationSlug}/menu`);
}
// Cart methods
async createCart(locationSlug) {
return this.request(`/locations/${locationSlug}/carts`, {
method: 'POST',
body: JSON.stringify({ marketplaceId: 'stripe' })
});
}
async addToCart(locationSlug, cartId, productId, quantity) {
return this.request(`/locations/${locationSlug}/carts/${cartId}/cart-item`, {
method: 'POST',
body: JSON.stringify({ id: productId, quantity })
});
}
async getCart(locationSlug, cartId) {
return this.request(`/locations/${locationSlug}/carts/${cartId}`);
}
// Order methods
async createOrder(cartId, customerInfo) {
return this.request('/orders', {
method: 'POST',
body: JSON.stringify({
cartId,
customer: customerInfo
})
});
}
async getOrder(orderId) {
return this.request(`/orders/${orderId}`);
}
}
// React Native component
import React, { useState, useEffect } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
Image,
StyleSheet,
Alert
} from 'react-native';
const MenuScreen = ({ navigation, route }) => {
const { locationSlug } = route.params;
const [menuItems, setMenuItems] = useState([]);
const [loading, setLoading] = useState(true);
const [cart, setCart] = useState(null);
const apiClient = new CraveApiClient(
'your-api-key',
'https://api.yourrestaurant.com/api/v1'
);
useEffect(() => {
loadMenuAndCart();
}, []);
const loadMenuAndCart = async () => {
try {
const [menuData, cartData] = await Promise.all([
apiClient.getLocationMenu(locationSlug),
apiClient.createCart(locationSlug)
]);
setMenuItems(menuData.items || []);
setCart(cartData);
} catch (error) {
Alert.alert('Error', 'Failed to load menu');
} finally {
setLoading(false);
}
};
const handleAddToCart = async (item) => {
try {
await apiClient.addToCart(locationSlug, cart.id, item.id, 1);
Alert.alert('Success', `${item.name} added to cart`);
} catch (error) {
Alert.alert('Error', 'Failed to add item to cart');
}
};
const renderMenuItem = ({ item }) => (
<View style={styles.menuItem}>
<Image source={{ uri: item.image }} style={styles.itemImage} />
<View style={styles.itemInfo}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemDescription}>{item.description}</Text>
<View style={styles.itemFooter}>
<Text style={styles.itemPrice}>${item.price}</Text>
<TouchableOpacity
style={[styles.addButton, !item.available && styles.disabledButton]}
onPress={() => handleAddToCart(item)}
disabled={!item.available}
>
<Text style={styles.buttonText}>
{item.available ? 'Add to Cart' : 'Unavailable'}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
if (loading) {
return (
<View style={styles.loading}>
<Text>Loading menu...</Text>
</View>
);
}
return (
<View style={styles.container}>
<FlatList
data={menuItems}
renderItem={renderMenuItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.menuList}
/>
<TouchableOpacity
style={styles.cartButton}
onPress={() => navigation.navigate('Cart', { cart })}
>
<Text style={styles.cartButtonText}>View Cart</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5'
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
menuList: {
padding: 16
},
menuItem: {
backgroundColor: 'white',
borderRadius: 8,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3
},
itemImage: {
width: '100%',
height: 200,
borderTopLeftRadius: 8,
borderTopRightRadius: 8
},
itemInfo: {
padding: 16
},
itemName: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8
},
itemDescription: {
fontSize: 14,
color: '#666',
marginBottom: 12
},
itemFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
},
itemPrice: {
fontSize: 16,
fontWeight: 'bold',
color: '#2196F3'
},
addButton: {
backgroundColor: '#2196F3',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 4
},
disabledButton: {
backgroundColor: '#ccc'
},
buttonText: {
color: 'white',
fontWeight: 'bold'
},
cartButton: {
backgroundColor: '#2196F3',
margin: 16,
padding: 16,
borderRadius: 8,
alignItems: 'center'
},
cartButtonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold'
}
});
export default MenuScreen;
// components/AnalyticsDashboard.jsx
import React, { useState, useEffect } from 'react';
import { Line, Bar, Doughnut } from 'react-chartjs-2';
const AnalyticsDashboard = ({ locationId }) => {
const [analytics, setAnalytics] = useState(null);
const [timeRange, setTimeRange] = useState('7d');
const [loading, setLoading] = useState(true);
useEffect(() => {
loadAnalytics();
}, [timeRange]);
const loadAnalytics = async () => {
try {
const endDate = new Date();
const startDate = new Date();
switch (timeRange) {
case '24h':
startDate.setHours(startDate.getHours() - 24);
break;
case '7d':
startDate.setDate(startDate.getDate() - 7);
break;
case '30d':
startDate.setDate(startDate.getDate() - 30);
break;
case '90d':
startDate.setDate(startDate.getDate() - 90);
break;
}
const response = await fetch(`/api/analytics/dashboard?locationId=${locationId}&startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}`);
const data = await response.json();
setAnalytics(data);
} catch (error) {
console.error('Failed to load analytics:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading analytics...</div>;
const revenueChartData = {
labels: analytics.daily.map(d => new Date(d.date).toLocaleDateString()),
datasets: [{
label: 'Revenue',
data: analytics.daily.map(d => d.revenue),
borderColor: '#2196F3',
backgroundColor: 'rgba(33, 150, 243, 0.1)',
fill: true
}]
};
const ordersChartData = {
labels: analytics.daily.map(d => new Date(d.date).toLocaleDateString()),
datasets: [{
label: 'Orders',
data: analytics.daily.map(d => d.orders),
backgroundColor: '#4CAF50'
}]
};
const popularItemsData = {
labels: analytics.popularItems.map(item => item.name),
datasets: [{
data: analytics.popularItems.map(item => item.quantity),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
};
return (
<div className="analytics-dashboard">
<div className="dashboard-header">
<h1>Restaurant Analytics</h1>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="time-range-select"
>
<option value="24h">Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="90d">Last 90 Days</option>
</select>
</div>
<div className="metrics-grid">
<div className="metric-card">
<h3>Total Revenue</h3>
<p className="metric-value">${analytics.totalRevenue.toFixed(2)}</p>
<p className="metric-change">
{analytics.revenueChange > 0 ? '+' : ''}
{analytics.revenueChange.toFixed(1)}% vs previous period
</p>
</div>
<div className="metric-card">
<h3>Total Orders</h3>
<p className="metric-value">{analytics.totalOrders}</p>
<p className="metric-change">
{analytics.ordersChange > 0 ? '+' : ''}
{analytics.ordersChange.toFixed(1)}% vs previous period
</p>
</div>
<div className="metric-card">
<h3>Average Order Value</h3>
<p className="metric-value">${analytics.averageOrderValue.toFixed(2)}</p>
<p className="metric-change">
{analytics.aovChange > 0 ? '+' : ''}
{analytics.aovChange.toFixed(1)}% vs previous period
</p>
</div>
<div className="metric-card">
<h3>Customer Satisfaction</h3>
<p className="metric-value">{analytics.customerSatisfaction.toFixed(1)}/5</p>
<p className="metric-change">
Based on {analytics.reviewCount} reviews
</p>
</div>
</div>
<div className="charts-grid">
<div className="chart-container">
<h3>Revenue Trend</h3>
<Line data={revenueChartData} />
</div>
<div className="chart-container">
<h3>Daily Orders</h3>
<Bar data={ordersChartData} />
</div>
<div className="chart-container">
<h3>Popular Items</h3>
<Doughnut data={popularItemsData} />
</div>
<div className="chart-container">
<h3>Order Status Distribution</h3>
<div className="status-breakdown">
{analytics.orderStatuses.map(status => (
<div key={status.status} className="status-item">
<span className={`status-indicator ${status.status}`}></span>
<span className="status-label">{status.status}</span>
<span className="status-count">{status.count}</span>
</div>
))}
</div>
</div>
</div>
<div className="insights-section">
<h3>AI-Powered Insights</h3>
<div className="insights-grid">
{analytics.insights.map((insight, index) => (
<div key={index} className="insight-card">
<div className={`insight-icon ${insight.type}`}>
{insight.icon}
</div>
<div className="insight-content">
<h4>{insight.title}</h4>
<p>{insight.description}</p>
{insight.action && (
<button className="insight-action">
{insight.action}
</button>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default AnalyticsDashboard;