Skip to content

Instantly share code, notes, and snippets.

@esafwan
Created February 5, 2025 22:25
Show Gist options
  • Save esafwan/764ab74d87834241bb8b0cc9c032ae94 to your computer and use it in GitHub Desktop.
Save esafwan/764ab74d87834241bb8b0cc9c032ae94 to your computer and use it in GitHub Desktop.
# Restaurant POS Application
 
This is a minimal yet fully functional restaurant POS application built with React. It includes features like item grid with category filtering, table selection, and a cart system, along with a payment modal that supports multiple payment types.
 
## Dir Structure:

public
└── favicon.svg
src
├── components
│ ├── Cart.jsx
│ ├── ItemGrid.jsx
│ └── TableSelection.jsx
├── App.css
├── App.jsx
├── data.js
└── index.jsx
.gitignore
.replit
README.md
generated-icon.png
index.html
package-lock.json
package.json
tsconfig.json
vite.config.js

File 1: index.html

Copy

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React + TypeScript + Replit</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/index.jsx"></script>
  </body>
</html>

File 2: README.md

 
[React](https://reactjs.org/) is a popular JavaScript library for building user interfaces.
 
[Vite](https://vitejs.dev/) is a blazing fast frontend build tool that includes features like Hot Module Reloading (HMR), optimized builds, and TypeScript support out of the box.
 
Using the two in conjunction is one of the fastest ways to build a web app.
 

File 3: package.json

{
  "name": "react-javascript",
  "version": "1.0.0",
  "type": "module",
  "description": "React TypeScript on Replit, using Vite bundler",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^18.2.37",
    "@types/react-dom": "^18.2.15",
    "@vitejs/plugin-react": "^4.2.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "typescript": "^5.2.2",
    "vite": "^5.0.0"
  }
}

File 4: src/index.jsx

JavaScript


Copy

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
 
ReactDOM.createRoot(document.getElementById('root')).render(
	<React.StrictMode>
		<App />
	</React.StrictMode>
)

File 5: src/App.jsx

import { useState, useEffect } from 'react'
import './App.css'
import ItemGrid from './components/ItemGrid'
import TableSelection from './components/TableSelection'
import Cart from './components/Cart'
import { categories, items, tables } from './data'
 
export default function App() {
  const [selectedTable, setSelectedTable] = useState(null)
  const [tableOrders, setTableOrders] = useState({})
  const [activeOrderId, setActiveOrderId] = useState(null)
 
  useEffect(() => {
    const savedOrders = localStorage.getItem('tableOrders')
    if (savedOrders) {
      setTableOrders(JSON.parse(savedOrders))
    }
  }, [])
 
  useEffect(() => {
    localStorage.setItem('tableOrders', JSON.stringify(tableOrders))
  }, [tableOrders])
 
  const addToCart = (item) => {
    if (!selectedTable) return
    const tableId = selectedTable.id
    const currentOrders = tableOrders[tableId] || {}
    const orderId = activeOrderId || Date.now()
    
    setTableOrders(prev => ({
      ...prev,
      [tableId]: {
        ...currentOrders,
        [orderId]: [...(currentOrders[orderId] || []), {...item, id: Date.now()}]
      }
    }))
    
    if (!activeOrderId) {
      setActiveOrderId(orderId)
    }
  }
 
  const removeFromCart = (itemId) => {
    if (!selectedTable || !activeOrderId) return
    const tableId = selectedTable.id
    setTableOrders(prev => ({
      ...prev,
      [tableId]: {
        ...prev[tableId],
        [activeOrderId]: prev[tableId][activeOrderId].filter(item => item.id !== itemId)
      }
    }))
  }
 
  const clearCart = () => {
    if (!selectedTable || !activeOrderId) return
    const tableId = selectedTable.id
    setTableOrders(prev => {
      const newOrders = {...prev}
      delete newOrders[tableId][activeOrderId]
      return newOrders
    })
    setActiveOrderId(null)
  }
 
  const createNewOrder = () => {
    if (!selectedTable) return
    setActiveOrderId(Date.now())
  }
 
  const switchOrder = (orderId) => {
    setActiveOrderId(orderId)
  }
 
  const currentItems = selectedTable && activeOrderId 
    ? tableOrders[selectedTable.id]?.[activeOrderId] || []
    : []
 
  const currentTableOrders = selectedTable 
    ? Object.keys(tableOrders[selectedTable.id] || {})
    : []
 
  return (
    <main className="pos-container">
      <section className="items-section">
        <ItemGrid 
          items={items}
          categories={categories}
          selectedCategory={'all'}
          setSelectedCategory={() => {}}
          addToCart={addToCart}
        />
      </section>
      <section className="tables-section">
        <TableSelection 
          tables={tables}
          selectedTable={selectedTable}
          setSelectedTable={setSelectedTable}
        />
      </section>
      <section className="cart-section">
        <Cart 
          items={currentItems}
          removeFromCart={removeFromCart}
          selectedTable={selectedTable}
          clearCart={clearCart}
          orders={currentTableOrders}
          activeOrderId={activeOrderId}
          onSwitchOrder={switchOrder}
          onNewOrder={createNewOrder}
        />
      </section>
    </main>
  )
}

File 6: src/App.css

/* CSS Reset */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
 
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
}
 
/* Main Layout */
.pos-container {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: 20px;
  padding: 20px;
  height: 100vh;
  background: #f5f5f5;
}
 
.items-section,
.tables-section,
.cart-section {
  background: #ffffff;
  border-radius: 6px;
  padding: 24px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  overflow-y: auto;
}
 
button {
  padding: 8px 16px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  background: #007AFF;
  color: white;
  font-weight: 500;
  transition: all 0.2s ease;
  font-size: 14px;
}
 
button:hover {
  background: #0063CC;
  transform: translateY(-1px);
}
 
.category-filter {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
 
.items-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 15px;
}
 
.item-card {
  background: #f5f5f7;
  padding: 16px;
  border-radius: 6px;
  text-align: center;
  transition: transform 0.2s ease;
}
 
.item-card:hover {
  transform: translateY(-2px);
}
 
.item-card h3 {
  margin-bottom: 8px;
  font-size: 16px;
}
 
.tables-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}
 
.table-card {
  padding: 16px;
  background: #f5f5f7;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
}
 
.table-card.selected {
  background: #E3F2FD;
  border-color: #007AFF;
  transform: translateY(-2px);
}
 
.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}
 
.cart-total {
  margin-top: 20px;
  font-weight: bold;
  text-align: right;
}
 
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}
 
.modal-content {
  background: white;
  padding: 24px;
  border-radius: 6px;
  width: 90%;
  max-width: 500px;
}
 
.payment-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin: 20px 0;
}
 
.payment-input {
  width: 100%;
  padding: 10px;
  border: 1px solid #e1e1e1;
  border-radius: 6px;
  font-size: 14px;
  background: #f5f5f7;
}
 
.payment-summary {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  margin: 20px 0;
}
 
.payment-label {
  font-size: 14px;
  color: #666;
  margin-bottom: 4px;
}
 
.pay-button {
  width: 100%;
  padding: 12px;
  margin-top: 20px;
  background: #007AFF;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  font-weight: 500;
}
 
.search-input {
  width: 100%;
  padding: 10px;
  border: 1px solid #e1e1e1;
  border-radius: 6px;
  margin-bottom: 15px;
  font-size: 14px;
  background: #f5f5f7;
  transition: all 0.2s ease;
}
 
.search-input:focus {
  outline: none;
  border-color: #007AFF;
  background: white;
}
 
.room-select {
  width: 100%;
  padding: 10px;
  border: 1px solid #e1e1e1;
  border-radius: 6px;
  margin-bottom: 15px;
  font-size: 14px;
  background: #f5f5f7;
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007AFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 8px auto;
}
 
.room-select:focus {
  outline: none;
  border-color: #007AFF;
  background-color: white;
}
 
.table-controls {
  margin-bottom: 20px;
}
 
.order-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
  overflow-x: auto;
  padding-bottom: 10px;
}
 
.order-controls button {
  flex-shrink: 0;
  padding: 6px 12px;
  font-size: 13px;
}
 
.cart-empty {
  text-align: center;
  color: #666;
  margin-top: 20px;
}

File 7: src/components/ItemGrid.jsx

import { useState } from 'react'
 
export default function ItemGrid({ items, categories, selectedCategory, setSelectedCategory, addToCart }) {
  const [searchQuery, setSearchQuery] = useState('')
  
  const filteredItems = items
    .filter(item => selectedCategory === 'all' || item.category === selectedCategory)
    .filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
 
  return (
    <div>
      <input
        type="search"
        placeholder="Search items..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        className="search-input"
      />
      <div className="category-filter">
        <button 
          onClick={() => setSelectedCategory('all')}
          style={{ background: selectedCategory === 'all' ? '#357abd' : '#4a90e2' }}
        >
          All
        </button>
        {categories.map(category => (
          <button
            key={category}
            onClick={() => setSelectedCategory(category)}
            style={{ background: selectedCategory === category ? '#357abd' : '#4a90e2' }}
          >
            {category}
          </button>
        ))}
      </div>
      <div className="items-grid">
        {filteredItems.map(item => (
          <div key={item.id} className="item-card">
            <h3>{item.name}</h3>
            <p>${item.price.toFixed(2)}</p>
            <button onClick={() => addToCart(item)}>Add to Cart</button>
          </div>
        ))}
      </div>
    </div>
  )
}

File 8: src/components/TableSelection.jsx

import { useState } from 'react'
 
export default function TableSelection({ tables, selectedTable, setSelectedTable }) {
  const [searchQuery, setSearchQuery] = useState('')
  const [selectedRoom, setSelectedRoom] = useState('all')
  
  const rooms = ['all', ...new Set(tables.map(table => table.room))]
  
  const filteredTables = tables
    .filter(table => selectedRoom === 'all' || table.room === selectedRoom)
    .filter(table => table.number.toString().includes(searchQuery))
 
  return (
    <div>
      <h2>Tables</h2>
      <div className="table-controls">
        <input
          type="search"
          placeholder="Search table number..."
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          className="search-input"
        />
        <select 
          value={selectedRoom}
          onChange={(e) => setSelectedRoom(e.target.value)}
          className="room-select"
        >
          {rooms.map(room => (
            <option key={room} value={room}>
              {room === 'all' ? 'All Rooms' : room}
            </option>
          ))}
        </select>
      </div>
      <div className="tables-grid">
        {filteredTables.map(table => (
          <div
            key={table.id}
            className={`table-card ${selectedTable?.id === table.id ? 'selected' : ''}`}
            onClick={() => setSelectedTable(table)}
          >
            <h3>Table {table.number}</h3>
            <p>Seats: {table.seats}</p>
            <p>Room: {table.room}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

File 9: src/components/Cart.jsx

import { useState } from 'react'
 
export default function Cart({ 
  items, 
  removeFromCart, 
  selectedTable, 
  clearCart, 
  orders,
  activeOrderId,
  onSwitchOrder,
  onNewOrder
}) {
  const [showPayModal, setShowPayModal] = useState(false)
  const [payments, setPayments] = useState([])
  const [currentPayment, setCurrentPayment] = useState({ method: 'cash', amount: 0 })
 
  const total = items.reduce((sum, item) => sum + item.price, 0)
  const totalPaid = payments.reduce((sum, payment) => sum + payment.amount, 0)
  const remaining = total - totalPaid
 
  const handleAddPayment = () => {
    if (currentPayment.amount > 0) {
      setPayments([...payments, currentPayment])
      const nextMethod = payments.length === 0 ? 'card' : (payments.length === 1 ? 'upi' : 'cash');
      setCurrentPayment({ method: nextMethod, amount: Math.max(0, remaining - currentPayment.amount) });
    }
  };
 
  const handlePay = () => {
    if (totalPaid >= total) {
      clearCart()
      setPayments([])
      setShowPayModal(false)
    }
  }
 
  const handlePaymentChange = (method, amount) => {
    setCurrentPayment({ method, amount: parseFloat(amount) || 0 });
  }
 
  if (!selectedTable) {
    return <div className="cart-empty">Select a table to start an order</div>
  }
 
  return (
    <div>
      <h2>Table {selectedTable.number}</h2>
 
      <div className="order-controls">
        <button onClick={onNewOrder}>New Order</button>
        {orders.map(orderId => (
          <button 
            key={orderId}
            onClick={() => onSwitchOrder(orderId)}
            style={{ 
              background: activeOrderId === orderId ? '#0063CC' : '#f5f5f7',
              color: activeOrderId === orderId ? 'white' : '#333',
              margin: '5px'
            }}
          >
            Order #{orderId.toString().slice(-4)}
          </button>
        ))}
      </div>
 
      {items.map(item => (
        <div key={item.id} className="cart-item">
          <span>{item.name}</span>
          <div>
            <span>${item.price.toFixed(2)}</span>
            <button 
              onClick={() => removeFromCart(item.id)}
              style={{ marginLeft: '10px', background: '#dc3545' }}
            >
              ×
            </button>
          </div>
        </div>
      ))}
 
      <div className="cart-total">
        Total: ${total.toFixed(2)}
      </div>
 
      {items.length > 0 && (
        <button 
          onClick={() => setShowPayModal(true)}
          style={{ width: '100%', marginTop: '10px' }}
        >
          Pay
        </button>
      )}
 
      {showPayModal && (
        <div className="modal">
          <div className="modal-content">
            <h3>Payment</h3>
            <p>Total: ${total.toFixed(2)}</p>
            <p>Remaining: ${remaining.toFixed(2)}</p>
 
            <div className="payment-grid">
              <div>
                <div className="payment-label">Cash</div>
                <input
                  type="number"
                  className="payment-input"
                  value={currentPayment.method === 'cash' ? currentPayment.amount : ''}
                  onChange={(e) => handlePaymentChange('cash', e.target.value)}
                  placeholder="0"
                />
              </div>
              <div>
                <div className="payment-label">Card</div>
                <input
                  type="number"
                  className="payment-input"
                  value={currentPayment.method === 'card' ? currentPayment.amount : ''}
                  onChange={(e) => handlePaymentChange('card', e.target.value)}
                  placeholder="0"
                />
              </div>
              <div>
                <div className="payment-label">UPI</div>
                <input
                  type="number"
                  className="payment-input"
                  value={currentPayment.method === 'upi' ? currentPayment.amount : ''}
                  onChange={(e) => handlePaymentChange('upi', e.target.value)}
                  placeholder="0"
                />
              </div>
            </div>
 
            <div className="payment-summary">
              <div>
                <div className="payment-label">Total</div>
                <input
                  type="number"
                  className="payment-input"
                  value={total}
                  disabled
                />
              </div>
              <div>
                <div className="payment-label">Change</div>
                <input
                  type="number"
                  className="payment-input"
                  value={Math.max(0, totalPaid - total)}
                  disabled
                />
              </div>
            </div>
 
            {payments.length > 0 && (
              <div className="payments-list">
                {payments.map((payment, index) => (
                  <div key={index} className="payment-item">
                    <span>{payment.method}</span>
                    <span>${payment.amount.toFixed(2)}</span>
                  </div>
                ))}
              </div>
            )}
 
            <div className="modal-actions">
              <button onClick={() => setShowPayModal(false)}>Cancel</button>
              <button 
                onClick={handlePay}
                disabled={totalPaid < total}
                style={{ 
                  background: totalPaid < total ? '#ccc' : '#007AFF',
                  marginLeft: '10px'
                }}
              >
                Complete Payment
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

File 10: src/data.js

export const categories = ['Food', 'Drinks', 'Desserts', 'Appetizers', 'Specials']
 
export const items = [
  { id: 1, name: 'Burger', price: 12.99, category: 'Food' },
  { id: 2, name: 'Pizza', price: 15.99, category: 'Food' },
  { id: 3, name: 'Salad', price: 8.99, category: 'Food' },
  { id: 4, name: 'Soda', price: 2.99, category: 'Drinks' },
  { id: 5, name: 'Coffee', price: 3.99, category: 'Drinks' },
  { id: 6, name: 'Ice Cream', price: 5.99, category: 'Desserts' },
  { id: 7, name: 'Cake', price: 6.99, category: 'Desserts' },
  { id: 8, name: 'Pasta', price: 13.99, category: 'Food' },
  { id: 9, name: 'Tea', price: 2.99, category: 'Drinks' },
  { id: 10, name: 'Steak', price: 25.99, category: 'Food' },
  { id: 11, name: 'Fish & Chips', price: 16.99, category: 'Food' },
  { id: 12, name: 'Wings', price: 10.99, category: 'Appetizers' },
  { id: 13, name: 'Nachos', price: 9.99, category: 'Appetizers' },
  { id: 14, name: 'Smoothie', price: 4.99, category: 'Drinks' },
  { id: 15, name: 'Tiramisu', price: 7.99, category: 'Desserts' },
  { id: 16, name: 'Chicken Curry', price: 14.99, category: 'Food' },
  { id: 17, name: 'Mojito', price: 8.99, category: 'Drinks' },
  { id: 18, name: 'Cheese Plate', price: 12.99, category: 'Appetizers' },
  { id: 19, name: 'Chef Special', price: 29.99, category: 'Specials' },
  { id: 20, name: 'Seasonal Soup', price: 6.99, category: 'Appetizers' }
]
 
export const tables = [
  { id: 1, number: 1, seats: 2, room: 'Main Hall' },
  { id: 2, number: 2, seats: 4, room: 'Main Hall' },
  { id: 3, number: 3, seats: 4, room: 'Main Hall' },
  { id: 4, number: 4, seats: 6, room: 'Main Hall' },
  { id: 5, number: 5, seats: 2, room: 'Main Hall' },
  { id: 6, number: 6, seats: 8, room: 'Main Hall' },
  { id: 7, number: 7, seats: 2, room: 'Terrace' },
  { id: 8, number: 8, seats: 4, room: 'Terrace' },
  { id: 9, number: 9, seats: 6, room: 'Terrace' },
  { id: 10, number: 10, seats: 4, room: 'Terrace' },
  { id: 11, number: 11, seats: 2, room: 'Private Room' },
  { id: 12, number: 12, seats: 8, room: 'Private Room' },
  { id: 13, number: 14, seats: 4, room: 'Bar Area' },
  { id: 14, number: 15, seats: 4, room: 'Bar Area' },
  { id: 15, number: 16, seats: 2, room: 'Bar Area' }
]

This markdown file compiles all the components of your application along with the corresponding structure for restaurant pos in react.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment