Skip to content

Instantly share code, notes, and snippets.

@ekaone
Created May 7, 2025 10:42
Show Gist options
  • Select an option

  • Save ekaone/cf412ca74a8cd01f8cf50f3256638020 to your computer and use it in GitHub Desktop.

Select an option

Save ekaone/cf412ca74a8cd01f8cf50f3256638020 to your computer and use it in GitHub Desktop.
POS
import React, { useState, useEffect, useCallback } from 'react';
import { ChevronDown, ChevronUp, ShoppingCart, Trash2, CreditCard, DollarSign, X, Coffee, Pizza, IceCream, GlassWater, PlusCircle, MinusCircle, CheckCircle } from 'lucide-react';
// Mock Product Data
const initialProducts = [
{ id: 'F001', name: 'Margherita Pizza', price: 75000, category: 'Main Course', imageUrl: 'https://placehold.co/400x300/f87171/ffffff?text=Margherita' },
{ id: 'F002', name: 'Pepperoni Pizza', price: 85000, category: 'Main Course', imageUrl: 'https://placehold.co/400x300/fb923c/ffffff?text=Pepperoni' },
{ id: 'F003', name: 'Spaghetti Carbonara', price: 65000, category: 'Main Course', imageUrl: 'https://placehold.co/400x300/fdba74/1e293b?text=Carbonara' },
{ id: 'F004', name: 'Caesar Salad', price: 45000, category: 'Appetizer', imageUrl: 'https://placehold.co/400x300/a3e635/1e293b?text=Caesar+Salad' },
{ id: 'F005', name: 'Garlic Bread', price: 25000, category: 'Appetizer', imageUrl: 'https://placehold.co/400x300/bef264/1e293b?text=Garlic+Bread' },
{ id: 'D001', name: 'Chocolate Lava Cake', price: 35000, category: 'Dessert', imageUrl: 'https://placehold.co/400x300/7e22ce/ffffff?text=Lava+Cake' },
{ id: 'D002', name: 'Vanilla Ice Cream', price: 20000, category: 'Dessert', imageUrl: 'https://placehold.co/400x300/a855f7/ffffff?text=Ice+Cream' },
{ id: 'B001', name: 'Espresso', price: 22000, category: 'Beverage', imageUrl: 'https://placehold.co/400x300/2dd4bf/1e293b?text=Espresso' },
{ id: 'B002', name: 'Iced Latte', price: 28000, category: 'Beverage', imageUrl: 'https://placehold.co/400x300/5eead4/1e293b?text=Iced+Latte' },
{ id: 'B003', name: 'Mineral Water', price: 10000, category: 'Beverage', imageUrl: 'https://placehold.co/400x300/99f6e4/1e293b?text=Water' },
{ id: 'F006', name: 'Beef Burger', price: 55000, category: 'Main Course', imageUrl: 'https://placehold.co/400x300/fca5a5/1e293b?text=Beef+Burger' },
{ id: 'F007', name: 'Chicken Wings', price: 40000, category: 'Appetizer', imageUrl: 'https://placehold.co/400x300/86efac/1e293b?text=Chicken+Wings' },
{ id: 'D003', name: 'Cheesecake Slice', price: 30000, category: 'Dessert', imageUrl: 'https://placehold.co/400x300/c084fc/ffffff?text=Cheesecake' },
{ id: 'B004', name: 'Fresh Orange Juice', price: 25000, category: 'Beverage', imageUrl: 'https://placehold.co/400x300/67e8f9/1e293b?text=Orange+Juice' },
];
// Helper to format currency (Indonesian Rupiah)
const formatCurrency = (amount) => {
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(amount);
};
// Category Icons Mapping
const categoryIcons = {
'All': <ShoppingCart size={20} className="mr-2" />,
'Main Course': <Pizza size={20} className="mr-2" />,
'Appetizer': <Coffee size={20} className="mr-2" />, // Using Coffee as a stand-in for general appetizer
'Dessert': <IceCream size={20} className="mr-2" />,
'Beverage': <GlassWater size={20} className="mr-2" />,
};
// Main App Component
function App() {
const [products] = useState(initialProducts);
const [categories, setCategories] = useState(['All']);
const [selectedCategory, setSelectedCategory] = useState('All');
const [cart, setCart] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [orderNumber, setOrderNumber] = useState(1001); // Mock order number
// Extract categories from products on mount
useEffect(() => {
const uniqueCategories = ['All', ...new Set(products.map(p => p.category))];
setCategories(uniqueCategories);
}, [products]);
// Filter products based on selected category and search term
const filteredProducts = products.filter(product => {
const categoryMatch = selectedCategory === 'All' || product.category === selectedCategory;
const searchMatch = product.name.toLowerCase().includes(searchTerm.toLowerCase());
return categoryMatch && searchMatch;
});
// Add item to cart or increment quantity
const addToCart = (product) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
return prevCart.map(item =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
}
return [...prevCart, { ...product, quantity: 1 }];
});
};
// Decrement item quantity or remove from cart
const decrementItem = (productId) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === productId);
if (existingItem && existingItem.quantity > 1) {
return prevCart.map(item =>
item.id === productId ? { ...item, quantity: item.quantity - 1 } : item
);
}
return prevCart.filter(item => item.id !== productId);
});
};
// Remove item from cart
const removeFromCart = (productId) => {
setCart(prevCart => prevCart.filter(item => item.id !== productId));
};
// Calculate total items in cart
const totalItemsInCart = cart.reduce((total, item) => total + item.quantity, 0);
// Calculate total price of items in cart
const totalPrice = cart.reduce((total, item) => total + item.price * item.quantity, 0);
// Handle payment process
const handlePayment = (paymentMethod) => {
console.log(`Processing payment with ${paymentMethod}... Total: ${formatCurrency(totalPrice)}`);
// Simulate payment success
setPaymentSuccess(true);
setShowPaymentModal(true);
// In a real app, you'd integrate with a payment gateway here.
};
// Close payment modal and reset cart
const closePaymentModal = useCallback(() => {
setShowPaymentModal(false);
if (paymentSuccess) {
setCart([]);
setOrderNumber(prev => prev + 1); // Increment order number for next order
}
setPaymentSuccess(false); // Reset payment success status
}, [paymentSuccess]);
// Product Card Component
const ProductCard = ({ product }) => (
<div className="bg-white rounded-xl shadow-lg overflow-hidden transform hover:scale-105 transition-transform duration-300 ease-in-out">
<img
src={product.imageUrl}
alt={product.name}
className="w-full h-40 object-cover"
onError={(e) => { e.target.onerror = null; e.target.src="https://placehold.co/400x300/cccccc/ffffff?text=Image+Not+Found"; }}
/>
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-1 truncate" title={product.name}>{product.name}</h3>
<p className="text-sm text-gray-500 mb-2">{product.category}</p>
<p className="text-xl font-bold text-indigo-600 mb-3">{formatCurrency(product.price)}</p>
<button
onClick={() => addToCart(product)}
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center justify-center"
>
<PlusCircle size={18} className="mr-2" /> Add to Cart
</button>
</div>
</div>
);
// Cart Item Component
const CartItemCard = ({ item }) => (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg mb-2 shadow-sm">
<div className="flex items-center">
<img
src={item.imageUrl}
alt={item.name}
className="w-12 h-12 object-cover rounded-md mr-3"
onError={(e) => { e.target.onerror = null; e.target.src="https://placehold.co/50x50/cccccc/ffffff?text=N/A"; }}
/>
<div>
<h4 className="font-semibold text-sm text-gray-700">{item.name}</h4>
<p className="text-xs text-gray-500">{formatCurrency(item.price)}</p>
</div>
</div>
<div className="flex items-center">
<button onClick={() => decrementItem(item.id)} className="p-1 text-indigo-600 hover:text-indigo-800 rounded-full hover:bg-indigo-100 transition-colors">
<MinusCircle size={20} />
</button>
<span className="mx-2 font-medium text-gray-700 w-5 text-center">{item.quantity}</span>
<button onClick={() => addToCart(item)} className="p-1 text-indigo-600 hover:text-indigo-800 rounded-full hover:bg-indigo-100 transition-colors">
<PlusCircle size={20} />
</button>
<button onClick={() => removeFromCart(item.id)} className="ml-3 p-1 text-red-500 hover:text-red-700 rounded-full hover:bg-red-100 transition-colors">
<Trash2 size={18} />
</button>
</div>
</div>
);
// Payment Modal Component
const PaymentModal = () => (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 transition-opacity duration-300">
<div className="bg-white p-8 rounded-xl shadow-2xl w-full max-w-md text-center transform scale-100 transition-transform duration-300">
{paymentSuccess ? (
<>
<CheckCircle size={64} className="text-green-500 mx-auto mb-6" />
<h2 className="text-2xl font-bold text-gray-800 mb-3">Payment Successful!</h2>
<p className="text-gray-600 mb-2">Order Number: <span className="font-semibold">#{orderNumber}</span></p>
<p className="text-gray-600 mb-6">Thank you for your purchase. Your order is being prepared.</p>
<button
onClick={closePaymentModal}
className="w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-200"
>
Close & New Order
</button>
</>
) : (
<>
<X size={64} className="text-red-500 mx-auto mb-6" />
<h2 className="text-2xl font-bold text-gray-800 mb-3">Payment Failed!</h2>
<p className="text-gray-600 mb-6">Something went wrong with the payment. Please try again.</p>
<button
onClick={closePaymentModal}
className="w-full bg-red-500 hover:bg-red-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-200"
>
Try Again
</button>
</>
)}
</div>
</div>
);
return (
<div className="min-h-screen bg-gray-100 flex flex-col font-sans">
{/* Header */}
<header className="bg-indigo-700 text-white shadow-lg sticky top-0 z-40">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight">F&B Point of Sale</h1>
<div className="relative">
<ShoppingCart size={28} />
{totalItemsInCart > 0 && (
<span className="absolute -top-2 -right-2 bg-pink-500 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
{totalItemsInCart}
</span>
)}
</div>
</div>
</header>
{/* Main Content Area */}
<main className="flex-grow container mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex flex-col lg:flex-row gap-6">
{/* Products Section (Left/Top on mobile) */}
<div className="lg:w-2/3">
{/* Search and Category Filters */}
<div className="mb-6 p-4 bg-white rounded-xl shadow-md">
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-shadow"
/>
<div className="mt-4 flex flex-wrap gap-2">
{categories.map(category => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 ease-in-out flex items-center
${selectedCategory === category
? 'bg-indigo-600 text-white shadow-md ring-2 ring-indigo-300'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 hover:shadow-sm'
}`}
>
{categoryIcons[category] || <ShoppingCart size={16} className="mr-2"/>} {category}
</button>
))}
</div>
</div>
{/* Product Grid */}
{filteredProducts.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-6">
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="text-center py-10">
<img src="https://placehold.co/128x128/e0e7ff/4338ca?text=:(" alt="No products found" className="mx-auto mb-4 rounded-lg" />
<p className="text-xl text-gray-500 font-semibold">No products found matching your criteria.</p>
<p className="text-gray-400">Try adjusting your search or category filters.</p>
</div>
)}
</div>
{/* Order Summary Section (Right/Bottom on mobile) */}
<div className="lg:w-1/3">
<div className="bg-white rounded-xl shadow-lg p-6 sticky top-24"> {/* Sticky for desktop */}
<h2 className="text-2xl font-semibold text-gray-800 mb-6 border-b pb-3">Your Order</h2>
{cart.length === 0 ? (
<div className="text-center py-8">
<ShoppingCart size={48} className="text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">Your cart is empty.</p>
<p className="text-sm text-gray-400">Add items from the menu to get started.</p>
</div>
) : (
<>
<div className="max-h-80 overflow-y-auto pr-2 mb-4 custom-scrollbar"> {/* Scrollable cart items */}
{cart.map(item => (
<CartItemCard key={item.id} item={item} />
))}
</div>
<div className="border-t pt-4">
<div className="flex justify-between items-center mb-2 text-gray-700">
<span>Subtotal</span>
<span className="font-semibold">{formatCurrency(totalPrice)}</span>
</div>
<div className="flex justify-between items-center mb-2 text-gray-700">
<span>Tax (10%)</span>
<span className="font-semibold">{formatCurrency(totalPrice * 0.10)}</span>
</div>
<div className="flex justify-between items-center text-xl font-bold text-gray-800 mt-3 pt-3 border-t">
<span>Total</span>
<span>{formatCurrency(totalPrice * 1.10)}</span>
</div>
</div>
<div className="mt-6 space-y-3">
<button
onClick={() => handlePayment('Cash')}
className="w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center justify-center text-lg"
>
<DollarSign size={20} className="mr-2" /> Pay with Cash
</button>
<button
onClick={() => handlePayment('Card')}
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center justify-center text-lg"
>
<CreditCard size={20} className="mr-2" /> Pay with Card
</button>
</div>
</>
)}
</div>
</div>
</div>
</main>
{/* Footer */}
<footer className="bg-gray-800 text-gray-300 text-center p-4 mt-auto">
<p>&copy; {new Date().getFullYear()} Cool F&B POS. All rights reserved.</p>
</footer>
{/* Payment Modal */}
{showPaymentModal && <PaymentModal />}
{/* Custom Scrollbar CSS (Optional, for aesthetics) */}
<style jsx global>{`
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1; // Tailwind gray-300
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8; // Tailwind gray-400
}
`}</style>
</div>
);
}
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment