My Orders fixed

This commit is contained in:
Sidharth Prabhu
2026-04-16 14:42:39 +05:30
parent 001d6d85a0
commit 5d247a09e9
56 changed files with 8520 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
/* App level styles */
#root {
width: 100%;
margin: 0 auto;
}

87
ordering_site/src/App.tsx Normal file
View File

@@ -0,0 +1,87 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { CartProvider } from './contexts/CartContext';
import { AuthProvider } from './contexts/AuthContext';
import { FoodProvider } from './contexts/FoodContext';
import ProtectedRoute from './components/ProtectedRoute';
import HomeScreen from './pages/HomeScreen';
import CategoryScreen from './pages/CategoryScreen';
import ItemDetailScreen from './pages/ItemDetailScreen';
import CartScreen from './pages/CartScreen';
import CheckoutScreen from './pages/CheckoutScreen';
import SuccessScreen from './pages/SuccessScreen';
import MyOrdersScreen from './pages/MyOrdersScreen';
import LoginScreen from './pages/LoginScreen';
import ProfileScreen from './pages/ProfileScreen';
import ChangePinScreen from './pages/ChangePinScreen';
import StallDetailScreen from './pages/StallDetailScreen';
import './App.css';
function App() {
return (
<AuthProvider>
<FoodProvider>
<CartProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginScreen />} />
<Route path="/" element={
<ProtectedRoute>
<HomeScreen />
</ProtectedRoute>
} />
<Route path="/orders" element={
<ProtectedRoute>
<MyOrdersScreen />
</ProtectedRoute>
} />
<Route path="/category/:categoryId" element={
<ProtectedRoute>
<CategoryScreen />
</ProtectedRoute>
} />
<Route path="/item/:itemId" element={
<ProtectedRoute>
<ItemDetailScreen />
</ProtectedRoute>
} />
<Route path="/stall/:id" element={
<ProtectedRoute>
<StallDetailScreen />
</ProtectedRoute>
} />
<Route path="/cart" element={
<ProtectedRoute>
<CartScreen />
</ProtectedRoute>
} />
<Route path="/checkout" element={
<ProtectedRoute>
<CheckoutScreen />
</ProtectedRoute>
} />
<Route path="/success" element={
<ProtectedRoute>
<SuccessScreen />
</ProtectedRoute>
} />
<Route path="/profile" element={
<ProtectedRoute>
<ProfileScreen />
</ProtectedRoute>
} />
<Route path="/change-pin" element={
<ProtectedRoute>
<ChangePinScreen />
</ProtectedRoute>
} />
</Routes>
</Router>
</CartProvider>
</FoodProvider>
</AuthProvider>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,44 @@
.bottom-nav {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 600px;
height: 65px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1px solid var(--border);
z-index: 1001;
padding-bottom: env(safe-area-inset-bottom);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
color: var(--text-mid);
gap: 4px;
transition: all 0.2s ease;
}
.nav-item.active {
color: var(--primary);
}
.nav-label {
font-size: 11px;
font-weight: 600;
}
/* Add safe area padding to containers when nav is visible */
.container:has(.bottom-nav) {
padding-bottom: 75px !important;
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Home, ShoppingBag, User } from 'lucide-react';
import './BottomNav.css';
const BottomNav: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const navItems = [
{ id: 'home', icon: Home, label: 'Home', path: '/' },
{ id: 'orders', icon: ShoppingBag, label: 'My Orders', path: '/orders' },
{ id: 'profile', icon: User, label: 'Profile', path: '/profile' }
];
// Only show on main screens and stall details
const isMainScreen = ['/', '/orders', '/profile'].includes(location.pathname);
const isStallDetail = location.pathname.startsWith('/stall/');
if (!isMainScreen && !isStallDetail) {
return null;
}
return (
<nav className="bottom-nav">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<button
key={item.id}
className={`nav-item ${isActive ? 'active' : ''}`}
onClick={() => navigate(item.path)}
>
<Icon size={24} />
<span className="nav-label">{item.label}</span>
</button>
);
})}
</nav>
);
};
export default BottomNav;

View File

@@ -0,0 +1,119 @@
.cart-tab-container {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 48px);
max-width: 500px;
z-index: 1000;
animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
transition: bottom 0.3s ease;
}
.cart-tab-container.has-footer {
bottom: 90px;
}
@keyframes slideUp {
from {
transform: translate(-50%, 100%);
opacity: 0;
}
to {
transform: translate(-50%, 0);
opacity: 1;
}
}
.cart-tab {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(35, 22, 81, 0.12);
color: var(--text-dark);
}
.cart-left-info {
display: flex;
flex-direction: column;
gap: 1px;
cursor: pointer;
padding-right: 12px;
align-items: flex-start;
min-width: 80px;
}
.cart-stats {
display: flex;
align-items: center;
gap: 8px;
line-height: 1;
margin-bottom: 2px;
}
.cart-icon {
color: var(--text-mid);
flex-shrink: 0;
}
.cart-count {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-mid);
text-transform: uppercase;
letter-spacing: 0.5px;
line-height: 1;
}
.cart-total {
font-size: 1.25rem;
font-weight: 800;
color: var(--primary);
line-height: 1.1;
margin-top: -2px;
}
.place-order-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
background: var(--primary);
color: white;
border-radius: 12px;
font-weight: 700;
font-size: 15px;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 4px 15px rgba(35, 22, 81, 0.2);
}
.place-order-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(35, 22, 81, 0.3);
}
.place-order-btn:active {
transform: scale(0.95);
}
.chevron-icon {
animation: bounceRight 2s infinite;
}
@keyframes bounceRight {
0%, 20%, 50%, 80%, 100% {transform: translateX(0);}
40% {transform: translateX(4px);}
60% {transform: translateX(2px);}
}
.safe-area-bottom {
padding-bottom: 100px !important;
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { ChevronRight, ShoppingBag } from 'lucide-react';
import { useCart } from '../contexts/CartContext';
import './CartTab.css';
const CartTab: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { totalItems, totalPrice } = useCart();
if (totalItems === 0) return null;
const isItemPage = location.pathname.startsWith('/item/');
return (
<div className={`cart-tab-container ${isItemPage ? 'has-footer' : ''}`}>
<div className="cart-tab">
<div className="cart-left-info" onClick={() => navigate('/cart')}>
<div className="cart-stats">
<ShoppingBag size={16} className="cart-icon" />
<span className="cart-count">
{totalItems} {totalItems === 1 ? 'item' : 'items'}
</span>
</div>
<div className="cart-total">{totalPrice.toFixed(0)}</div>
</div>
<button className="place-order-btn" onClick={() => navigate('/checkout')}>
Place Order
<ChevronRight size={18} className="chevron-icon" />
</button>
</div>
</div>
);
};
export default CartTab;

View File

@@ -0,0 +1,175 @@
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: var(--surface);
position: sticky;
top: 0;
z-index: 100;
min-height: 64px;
border-bottom: 1px solid var(--border);
gap: 8px;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0; /* Important for text truncation */
}
.back-button {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg);
border-radius: 50%;
color: var(--text-dark);
flex-shrink: 0;
}
.header-logo {
height: 38px;
width: auto;
object-fit: contain;
cursor: pointer;
transition: opacity 0.2s;
}
.header-logo:hover {
opacity: 0.8;
}
.header-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-dark);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.location-picker {
display: flex;
flex-direction: column;
min-width: 0;
}
.location-main {
display: flex;
align-items: center;
gap: 4px;
}
.location-label {
font-weight: 700;
font-size: 0.95rem;
}
.location-sub {
font-size: 0.75rem;
color: var(--text-mid);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-right {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.user-profile-section {
display: flex;
align-items: center;
gap: 4px;
}
.user-name-header {
font-size: 0.8rem;
font-weight: 700;
color: var(--primary);
background-color: var(--primary-light);
padding: 4px 8px;
border-radius: 20px;
white-space: nowrap;
}
.icon-button {
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg);
border-radius: 50%;
color: var(--text-mid);
flex-shrink: 0;
}
.icon-button.profile {
background-color: var(--primary);
color: white;
}
.search-bar {
display: flex;
align-items: center;
gap: 10px;
background-color: rgba(35, 22, 81, 0.05); /* Light Purple Wash */
padding: 10px 14px;
border-radius: 12px;
border: 1px solid var(--border);
}
.cart-pill {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
background-color: var(--primary);
border-radius: 8px;
color: white;
font-weight: 600;
font-size: 0.75rem;
flex-shrink: 0;
}
.cart-count {
background-color: white;
color: #000000; /* Black text as requested earlier */
border-radius: 50%;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 900;
}
.cart-price {
white-space: nowrap;
}
.primary-color {
color: var(--primary);
}
@media (max-width: 360px) {
.location-sub, .cart-text {
display: none;
}
.user-name-header {
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
}

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Bell, User, ShoppingBag, LogOut } from 'lucide-react';
import { useCart } from '../contexts/CartContext';
import { useAuth } from '../contexts/AuthContext';
import ritLogo from '../assets/RIT_Logo.png';
import './Header.css';
interface HeaderProps {
title?: string;
onBack?: () => void;
showCart?: boolean;
}
const Header: React.FC<HeaderProps> = ({ title, onBack, showCart = true }) => {
const navigate = useNavigate();
const { totalItems, totalPrice } = useCart();
const { user, logout } = useAuth();
const handleBack = onBack || (() => navigate(-1));
return (
<header className="app-header">
<div className="header-left">
{onBack || (title && title !== "RIT Canteen") ? (
<button onClick={handleBack} className="back-button">
<ArrowLeft size={20} />
</button>
) : null}
{title && title !== "RIT Canteen" ? (
<h1 className="header-title">{title}</h1>
) : (
<img
src={ritLogo}
alt="RIT Logo"
className="header-logo"
onClick={() => navigate('/')}
/>
)}
</div>
<div className="header-right">
{user ? (
<div className="user-profile-section">
<button className="icon-button logout" onClick={() => {
if (window.confirm('Are you sure you want to logout?')) {
logout();
}
}}>
<LogOut size={18} />
</button>
</div>
) : (
<button className="icon-button profile" onClick={() => navigate('/login')}>
<User size={20} />
</button>
)}
<button className="icon-button">
<Bell size={20} />
</button>
{showCart && (
<button
className={`cart-pill ${totalItems > 0 ? 'active' : ''}`}
onClick={() => navigate('/cart')}
>
<ShoppingBag size={18} />
{totalItems > 0 ? (
<>
<span className="cart-count">{totalItems}</span>
<span className="cart-price">{totalPrice.toFixed(2)}</span>
</>
) : (
<span className="cart-text">Cart</span>
)}
</button>
)}
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,216 @@
.item-card {
display: flex;
padding: 16px;
border-bottom: 1px solid var(--border);
gap: 12px;
cursor: pointer;
min-width: 0;
}
.item-card.last {
border-bottom: none;
}
.item-card.out-of-stock {
opacity: 0.6;
filter: grayscale(0.4);
}
.out-of-stock-badge {
background-color: #f3f4f6;
color: #6b7280;
font-size: 0.65rem;
font-weight: 700;
padding: 1px 6px;
border-radius: 20px;
white-space: nowrap;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.item-labels {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.veg-icon {
width: 14px;
height: 14px;
border: 2px solid;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.veg-icon.veg {
border-color: var(--green);
}
.veg-icon.non-veg {
border-color: var(--red);
}
.veg-icon .dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.veg-icon.veg .dot {
background-color: var(--green);
}
.veg-icon.non-veg .dot {
background-color: var(--red);
}
.bestseller-badge {
background-color: #f5f0ff;
color: #7c3aed;
font-size: 0.65rem;
font-weight: 700;
padding: 1px 6px;
border-radius: 20px;
white-space: nowrap;
}
.item-name {
font-size: 1rem;
font-weight: 700;
color: var(--text-dark);
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.item-price {
font-size: 0.9rem;
font-weight: 700;
color: var(--text-dark);
margin-bottom: 4px;
}
.item-rating {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 6px;
}
.rating-text {
font-size: 0.75rem;
color: var(--text-mid);
}
.item-description {
font-size: 0.8rem;
color: var(--text-mid);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.item-image-container {
width: 100px;
height: 100px;
position: relative;
margin-bottom: 14px;
flex-shrink: 0;
}
@media (max-width: 360px) {
.item-image-container {
width: 80px;
height: 80px;
}
}
.item-image, .item-image-placeholder {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
background-color: var(--primary-light);
}
.item-image-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
border: 1px solid var(--border);
}
.placeholder-icon {
font-size: 1.8rem;
opacity: 0.6;
}
.add-to-cart-container {
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.12);
border: 1px solid var(--border);
min-width: 80px;
display: flex;
justify-content: center;
}
.add-button {
padding: 8px 12px;
font-weight: 800;
font-size: 0.85rem;
color: var(--primary);
width: 100%;
transition: all 0.2s ease;
}
.add-button:hover {
background-color: var(--primary-light);
}
.quantity-controls {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
}
.quantity-controls button {
width: 24px;
height: 24px;
background-color: var(--primary-light);
color: var(--primary);
border-radius: 6px;
font-size: 1.1rem;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
}
.quantity {
font-weight: 800;
font-size: 0.9rem;
color: var(--primary);
min-width: 16px;
text-align: center;
}

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import type { FoodItem } from '../types';
import { useCart } from '../contexts/CartContext';
import './ItemCard.css';
interface ItemCardProps {
item: FoodItem;
isLast?: boolean;
}
const ItemCard: React.FC<ItemCardProps> = ({ item, isLast }) => {
const navigate = useNavigate();
const { addToCart, updateQuantity, getItemQuantity } = useCart();
const quantity = getItemQuantity(item.id);
return (
<div
className={`item-card ${isLast ? 'last' : ''} ${item.stock === 0 ? 'out-of-stock' : ''}`}
onClick={() => navigate(`/item/${item.id}`)}
>
<div className="item-info">
<div className="item-labels">
<div className={`veg-icon ${item.isVeg ? 'veg' : 'non-veg'}`}>
<div className="dot" />
</div>
{item.isPopular && <span className="bestseller-badge">Popular</span>}
{item.stock === 0 && <span className="out-of-stock-badge">Out of Stock</span>}
</div>
<h3 className="item-name">{item.name}</h3>
<p className="item-price">{item.price.toFixed(2)}</p>
<p className="item-description">{item.description}</p>
</div>
<div className="item-image-container" onClick={(e) => e.stopPropagation()}>
{item.image ? (
<img src={item.image} alt={item.name} className="item-image" />
) : (
<div className="item-image-placeholder">
<span className="placeholder-icon">🍲</span>
</div>
)}
<div className="add-to-cart-container">
{quantity === 0 ? (
<button
className="add-button"
onClick={() => addToCart(item)}
disabled={item.stock === 0}
>
{item.stock === 0 ? 'SOLD OUT' : 'ADD'}
</button>
) : (
<div className="quantity-controls">
<button onClick={() => updateQuantity(item.id, -1)}></button>
<span className="quantity">{quantity}</span>
<button onClick={() => addToCart(item)}>+</button>
</div>
)}
</div>
</div>
</div>
);
};
export default ItemCard;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
};
export default ProtectedRoute;

View File

@@ -0,0 +1,121 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import type { User } from '../types';
interface AuthContextType {
user: User | null;
isLoading: boolean;
checkUserExists: (mobileNumber: string) => Promise<{ success: boolean; userExists: boolean; message: string }>;
login: (mobileNumber: string, pin: string) => Promise<{ success: boolean; message: string }>;
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
logout: () => void;
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const API_BASE_URL = `http://${window.location.hostname}:8080/api/auth`;
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
setIsLoading(false);
}, []);
const checkUserExists = async (mobileNumber: string) => {
try {
const response = await fetch(`${API_BASE_URL}/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobileNumber }),
});
return await response.json();
} catch (error) {
return { success: false, userExists: false, message: 'Network error. Please try again.' };
}
};
const login = async (mobileNumber: string, pin: string) => {
try {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobileNumber, pin }),
});
const data = await response.json();
if (data.success && data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
return { success: true, message: data.message };
}
return { success: false, message: data.message || 'Login failed' };
} catch (error) {
return { success: false, message: 'Network error. Please try again.' };
}
};
const register = async (mobileNumber: string, name: string, pin: string) => {
try {
const response = await fetch(`${API_BASE_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobileNumber, name, pin }),
});
const data = await response.json();
if (data.success && data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
return { success: true, message: data.message };
}
return { success: false, message: data.message || 'Registration failed' };
} catch (error) {
return { success: false, message: 'Network error. Please try again.' };
}
};
const logout = () => {
if (user) {
fetch(`${API_BASE_URL}/logout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobileNumber: user.mobileNumber }),
});
}
setUser(null);
localStorage.removeItem('user');
};
const changePin = async (currentPin: string, newPin: string) => {
if (!user) return { success: false, message: 'Not logged in' };
try {
const response = await fetch(`${API_BASE_URL}/change-pin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobileNumber: user.mobileNumber, currentPin, newPin }),
});
const data = await response.json();
return { success: data.success, message: data.message };
} catch (error) {
return { success: false, message: 'Network error. Please try again.' };
}
};
return (
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -0,0 +1,100 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import type { FoodItem, CartItem } from '../types';
interface CartContextType {
cart: CartItem[];
addToCart: (item: FoodItem) => void;
removeFromCart: (itemId: string) => void;
updateQuantity: (itemId: string, delta: number) => void;
clearCart: () => void;
getItemQuantity: (itemId: string) => number;
totalItems: number;
totalPrice: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [cart, setCart] = useState<CartItem[]>(() => {
const savedCart = localStorage.getItem('cart');
return savedCart ? JSON.parse(savedCart) : [];
});
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const addToCart = (item: FoodItem) => {
setCart((prevCart) => {
const existingIndex = prevCart.findIndex((i) => i.id === item.id);
if (existingIndex !== -1) {
const newCart = [...prevCart];
newCart[existingIndex] = {
...newCart[existingIndex],
quantity: newCart[existingIndex].quantity + 1,
};
return newCart;
}
return [...prevCart, { ...item, quantity: 1 }];
});
};
const removeFromCart = (itemId: string) => {
setCart((prevCart) => prevCart.filter((item) => item.id !== itemId));
};
const updateQuantity = (itemId: string, delta: number) => {
setCart((prevCart) => {
const index = prevCart.findIndex((item) => item.id === itemId);
if (index !== -1) {
const newCart = [...prevCart];
const newQty = newCart[index].quantity + delta;
if (newQty > 0) {
newCart[index] = { ...newCart[index], quantity: newQty };
return newCart;
} else {
return prevCart.filter((item) => item.id !== itemId);
}
}
return prevCart;
});
};
const clearCart = () => {
setCart([]);
localStorage.removeItem('cart');
};
const getItemQuantity = (itemId: string) => {
const item = cart.find((i) => i.id === itemId);
return item ? item.quantity : 0;
};
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
<CartContext.Provider
value={{
cart,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
getItemQuantity,
totalItems,
totalPrice,
}}
>
{children}
</CartContext.Provider>
);
};
export const useCart = () => {
const context = useContext(CartContext);
if (context === undefined) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};

View File

@@ -0,0 +1,145 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import type { FoodItem, Category, Stall } from '../types';
interface FoodContextType {
categories: Category[];
foodItems: FoodItem[];
stalls: Stall[];
isLoading: boolean;
error: string | null;
refreshData: () => Promise<void>;
}
const FoodContext = createContext<FoodContextType | undefined>(undefined);
const API_BASE_URL = `http://${window.location.hostname}:8080/api`;
const EMOJIS = ['🍦', '🍪', '🧃', '🍫', '🥨', '🥐', '🍹', '📦', '🍳', '🍱', '🍔', '🍕', '🥗', '🍲'];
const COLORS = ['#FFF0F6', '#FFF7ED', '#FFFFEB', '#FDF3EF', '#F0FDF4', '#FEFCE8', '#EFF6FF'];
export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [categories, setCategories] = useState<Category[]>([]);
const [foodItems, setFoodItems] = useState<FoodItem[]>([]);
const [stalls, setStalls] = useState<Stall[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
fetch(`${API_BASE_URL}/base-items?size=100`),
fetch(`${API_BASE_URL}/products?size=100`),
fetch(`${API_BASE_URL}/stalls/active`)
]);
if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) {
throw new Error('Failed to fetch data from server');
}
const baseItemsDataRaw = await baseItemsRes.json();
const productsDataRaw = await productsRes.json();
const stallsData = await stallsRes.json();
const baseItemsData = baseItemsDataRaw.content || baseItemsDataRaw;
const productsData = productsDataRaw.content || productsDataRaw;
// Map BaseItems to Categories
const mappedCategories: Category[] = baseItemsData.map((item: any, index: number) => ({
id: item.id.toString(),
description: item.description,
emoji: EMOJIS[index % EMOJIS.length],
image: item.imageData?.trim() ? (item.imageData.trim().startsWith('data:') ? item.imageData.trim() : `data:image/png;base64,${item.imageData.trim()}`) : '',
color: COLORS[index % COLORS.length]
}));
// Map Stalls and build a product-to-stall lookup map
const productToStallMap: Record<string, { id: string, name: string }> = {};
const categoryToStallMap: Record<string, { id: string, name: string }> = {};
const mappedStalls: Stall[] = stallsData.map((item: any) => {
const stallInfo = { id: item.id.toString(), name: item.name };
// Map category/baseItems to this stall for indirect lookup
if (item.baseItems) {
item.baseItems.forEach((bi: any) => {
if (!categoryToStallMap[bi.name.toLowerCase()]) {
categoryToStallMap[bi.name.toLowerCase()] = stallInfo;
}
});
}
// If this stall explicitly lists products, add them to map
if (item.products) {
item.products.forEach((p: any) => {
if (!productToStallMap[p.id.toString()]) {
productToStallMap[p.id.toString()] = stallInfo;
}
});
}
return {
id: stallInfo.id,
name: stallInfo.name,
description: item.description || 'Our delicious stall',
imageData: item.imageData?.trim() ? (item.imageData.trim().startsWith('data:') ? item.imageData.trim() : `data:image/png;base64,${item.imageData.trim()}`) : '',
active: item.active,
temporarilyClosed: item.temporarilyClosed
};
});
// Map Products to FoodItems using the lookup map as fallback
const mappedFoodItems: FoodItem[] = productsData.map((item: any) => {
const rawImg = item.imageData?.trim();
const finalImage = rawImg ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) : '';
const itemId = item.id.toString();
const stallFromBackend = item.stalls && item.stalls.length > 0 ? { id: item.stalls[0].id.toString(), name: item.stalls[0].name } : null;
const stallFromMap = productToStallMap[itemId];
const stallFromCategory = item.category ? categoryToStallMap[item.category.toLowerCase()] : null;
return {
id: itemId,
name: item.name,
description: item.description || 'No description available',
price: item.price || item.basePrice || 0,
category: item.category,
image: finalImage,
isVeg: item.veg,
isPopular: item.active,
stock: item.stock,
stallId: (stallFromBackend?.id || stallFromMap?.id || stallFromCategory?.id),
stallName: (stallFromBackend?.name || stallFromMap?.name || stallFromCategory?.name)
};
});
setCategories(mappedCategories);
setFoodItems(mappedFoodItems);
setStalls(mappedStalls);
} catch (err: any) {
setError(err.message);
console.error('Error fetching food data:', err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<FoodContext.Provider value={{ categories, foodItems, stalls, isLoading, error, refreshData: fetchData }}>
{children}
</FoodContext.Provider>
);
};
export const useFood = () => {
const context = useContext(FoodContext);
if (context === undefined) {
throw new Error('useFood must be used within a FoodProvider');
}
return context;
};

142
ordering_site/src/index.css Normal file
View File

@@ -0,0 +1,142 @@
:root {
--primary: #231651;
--primary-light: #f0f1ff;
--bg: #ffffff;
--surface: #ffffff;
--border: rgba(35, 22, 81, 0.1);
--green: #00B894;
--green-light: #E6FFFA;
--red: #ff4d4f;
--text-dark: #231651;
--text-mid: #4a3e7a;
--text-light: #7b71af;
--star: #f4c640;
--shadow: 0 4px 20px rgba(35, 22, 81, 0.05);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html, body {
width: 100%;
overflow-x: hidden;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: var(--bg);
color: var(--text-dark);
line-height: 1.5;
font-size: 16px;
}
button {
cursor: pointer;
border: none;
background: none;
font-family: inherit;
}
a {
text-decoration: none;
color: inherit;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
min-height: 100vh;
background-color: var(--surface);
position: relative;
box-shadow: 0 0 20px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
}
.safe-area-bottom {
padding-bottom: 100px; /* Increased for fixed footers */
}
@media (max-width: 480px) {
body {
font-size: 14px;
}
}
.loading-spinner {
padding: 20px;
color: var(--primary);
font-weight: 600;
}
/* Global QR Overlay Styles */
.qr-container {
position: relative;
display: inline-block;
}
.qr-wrapper-inner {
position: relative;
display: inline-block;
}
.qr-completed canvas,
.qr-expired canvas {
filter: blur(8px) !important;
opacity: 0.6 !important;
}
.qr-expired canvas {
filter: blur(12px) grayscale(100%) !important;
}
.qr-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 50 !important;
pointer-events: none;
}
.overlay-text {
background: #ef4444;
color: white;
padding: 8px 16px;
border-radius: 8px;
font-weight: 900;
font-size: 1.2rem;
transform: rotate(-15deg);
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
border: 2px solid white;
letter-spacing: 1px;
white-space: nowrap;
}
.overlay-text.expired {
background: #374151 !important;
}
.overlay-text.mini {
padding: 4px 8px;
font-size: 0.75rem;
border-radius: 4px;
}
.latest-qr-wrapper.qr-completed canvas,
.latest-qr-wrapper.qr-expired canvas {
filter: blur(4px) !important;
}
.latest-qr-wrapper.qr-expired canvas {
filter: blur(6px) grayscale(100%) !important;
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,233 @@
.empty-cart-container {
display: flex;
flex-direction: column;
}
.empty-cart-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 24px;
text-align: center;
background-color: var(--surface);
}
.empty-cart-icon {
color: var(--border);
margin-bottom: 24px;
}
.empty-title {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-dark);
margin-bottom: 8px;
}
.empty-subtitle {
font-size: 0.9rem;
color: var(--text-mid);
margin-bottom: 32px;
}
.primary-button {
background-color: var(--primary);
color: white;
padding: 12px 24px;
border-radius: 12px;
font-size: 1rem;
font-weight: 700;
}
.cart-page {
background-color: var(--bg);
}
.cart-items-section {
background-color: var(--surface);
padding: 16px;
margin-bottom: 10px;
}
.cart-item-row {
display: flex;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid var(--border);
}
.cart-item-row:last-child {
border-bottom: none;
}
.cart-item-image-box {
width: 70px;
height: 70px;
border-radius: 10px;
overflow: hidden;
background-color: var(--bg);
flex-shrink: 0;
}
.cart-item-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.cart-item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
}
.cart-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.cart-item-name {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-dark);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.remove-item {
color: var(--text-light);
padding: 4px;
flex-shrink: 0;
}
.cart-item-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-item-price {
font-size: 0.9rem;
font-weight: 700;
color: var(--text-dark);
}
.cart-quantity-controls {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 8px;
}
.cart-quantity-controls button {
color: var(--primary);
display: flex;
align-items: center;
}
.cart-quantity {
font-size: 0.85rem;
font-weight: 700;
color: var(--primary);
min-width: 18px;
text-align: center;
}
.bill-details {
background-color: var(--surface);
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.bill-row {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: var(--text-mid);
}
.bill-row.total {
border-top: 1px solid var(--border);
padding-top: 12px;
font-weight: 800;
color: var(--text-dark);
font-size: 1.1rem;
}
.free {
color: var(--green);
font-weight: 700;
}
.cart-footer {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 600px;
background-color: var(--surface);
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
border-top: 1px solid var(--border);
z-index: 101;
gap: 16px;
}
.footer-total {
display: flex;
flex-direction: column;
min-width: 0;
}
.items-count {
font-size: 0.75rem;
color: var(--text-mid);
}
.final-price {
font-size: 1.1rem;
font-weight: 800;
color: var(--text-dark);
}
.checkout-button {
background-color: var(--primary);
color: white;
padding: 12px 24px;
border-radius: 12px;
font-size: 1rem;
font-weight: 700;
flex: 1;
max-width: 200px;
text-align: center;
white-space: nowrap;
}
@media (max-width: 360px) {
.cart-footer {
padding: 10px 12px;
gap: 10px;
}
.checkout-button {
padding: 10px 16px;
font-size: 0.9rem;
}
.final-price {
font-size: 1rem;
}
}

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Trash2, ShoppingBag, Plus, Minus } from 'lucide-react';
import Header from '../components/Header';
import { useCart } from '../contexts/CartContext';
import './CartScreen.css';
const CartScreen: React.FC = () => {
const navigate = useNavigate();
const { cart, updateQuantity, removeFromCart, totalPrice, totalItems } = useCart();
if (cart.length === 0) {
return (
<div className="container empty-cart-container">
<Header title="Cart" />
<div className="empty-cart-content">
<ShoppingBag size={80} className="empty-cart-icon" />
<h2 className="empty-title">Your cart is empty</h2>
<p className="empty-subtitle">Look like you haven't added anything to your cart yet.</p>
<button className="primary-button" onClick={() => navigate('/')}>
Explore Menu
</button>
</div>
</div>
);
}
return (
<div className="container cart-page">
<Header title="Cart" showCart={false} />
<main className="safe-area-bottom">
<div className="cart-items-section">
{cart.map((item) => (
<div key={item.id} className="cart-item-row">
<div className="cart-item-image-box">
<img src={item.image} alt={item.name} className="cart-item-image" />
</div>
<div className="cart-item-info">
<div className="cart-item-header">
<h3 className="cart-item-name">{item.name}</h3>
<button onClick={() => removeFromCart(item.id)} className="remove-item">
<Trash2 size={16} />
</button>
</div>
<div className="cart-item-footer">
<span className="cart-item-price">₹{(item.price * item.quantity).toFixed(2)}</span>
<div className="cart-quantity-controls">
<button onClick={() => updateQuantity(item.id, -1)}>
<Minus size={14} />
</button>
<span className="cart-quantity">{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, 1)}>
<Plus size={14} />
</button>
</div>
</div>
</div>
</div>
))}
</div>
<div className="bill-details">
<h2 className="section-title">Bill Details</h2>
<div className="bill-row">
<span>Item Total</span>
<span>₹{totalPrice.toFixed(2)}</span>
</div>
<div className="bill-row">
<span>Delivery Fee</span>
<span className="free">FREE</span>
</div>
<div className="bill-row">
<span>Taxes and Charges</span>
<span>₹2.50</span>
</div>
<div className="bill-row total">
<span>To Pay</span>
<span>₹{(totalPrice + 2.5).toFixed(2)}</span>
</div>
</div>
</main>
<div className="cart-footer">
<div className="footer-total">
<span className="items-count">{totalItems} {totalItems === 1 ? 'Item' : 'Items'}</span>
<span className="final-price">₹{(totalPrice + 2.5).toFixed(2)}</span>
</div>
<button
className="checkout-button"
onClick={() => navigate('/checkout')}
>
Checkout
</button>
</div>
</div>
);
};
export default CartScreen;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import Header from '../components/Header';
import ItemCard from '../components/ItemCard';
import CartTab from '../components/CartTab';
import { useFood } from '../contexts/FoodContext';
const CategoryScreen: React.FC = () => {
const { categoryId } = useParams<{ categoryId: string }>(); // categoryId is actually categoryName now
const { categories, foodItems, isLoading } = useFood();
const category = categories.find((c) => c.name === categoryId);
const items = foodItems.filter((item) => item.category === categoryId);
if (isLoading && categories.length === 0) {
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
}
if (!category && !isLoading) return <div className="container" style={{ padding: 24 }}>Category not found: {categoryId}</div>;
return (
<div className="container">
<Header title={category?.name || categoryId} />
<main className="safe-area-bottom">
<div className="items-list">
{items.map((item, index) => (
<ItemCard
key={item.id}
item={item}
isLast={index === items.length - 1}
/>
))}
{items.length === 0 && !isLoading && (
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-mid)' }}>
No items found in this category.
</div>
)}
</div>
</main>
<CartTab />
</div>
);
};
export default CategoryScreen;

View File

@@ -0,0 +1,161 @@
.change-pin-page {
background-color: var(--bg);
min-height: 100vh;
}
.change-pin-content {
padding: 24px 16px;
display: flex;
justify-content: center;
}
.change-pin-card {
width: 100%;
max-width: 400px;
background: var(--surface);
border-radius: 28px;
padding: 32px 24px;
box-shadow: var(--shadow);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.step-indicator {
font-size: 11px;
font-weight: 800;
color: var(--text-light);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.step-title {
font-size: 22px;
font-weight: 850;
color: var(--text-dark);
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.step-desc {
font-size: 14px;
color: var(--text-mid);
line-height: 1.5;
margin-bottom: 32px;
}
.pin-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 13px;
font-weight: 700;
color: var(--text-mid);
margin-left: 4px;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.input-icon {
position: absolute;
left: 16px;
color: var(--text-light);
}
.pin-form input {
width: 100%;
padding: 16px 16px 16px 48px;
border-radius: 16px;
border: 2px solid #f0f2f5;
background-color: #f0f2f5;
font-size: 18px;
font-weight: 700;
color: var(--text-dark);
outline: none;
transition: all 0.3s ease;
letter-spacing: 4px;
}
.pin-form input:focus {
background-color: #ffffff;
border-color: var(--primary-light);
box-shadow: 0 0 0 4px rgba(35, 22, 81, 0.05);
}
.primary-button {
width: 100%;
padding: 16px;
background-color: var(--primary);
color: white;
border-radius: 16px;
font-size: 16px;
font-weight: 800;
margin-top: 8px;
box-shadow: 0 8px 20px rgba(35, 22, 81, 0.2);
transition: all 0.2s ease;
}
.primary-button:disabled {
opacity: 0.6;
box-shadow: none;
}
.primary-button:active:not(:disabled) {
transform: scale(0.98);
}
.back-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 700;
color: var(--text-mid);
margin-bottom: 16px;
width: fit-content;
}
.success-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 24px 0;
}
.success-check {
color: var(--green);
margin-bottom: 24px;
animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes scaleIn {
0% { transform: scale(0); }
70% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.error-message {
padding: 12px;
background-color: #fff1f2;
color: #ff4d4f;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
border: 1px solid rgba(255, 77, 79, 0.1);
text-align: center;
}

View File

@@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Lock, ChevronLeft, CheckCircle2 } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import Header from '../components/Header';
import './ChangePinScreen.css';
const ChangePinScreen: React.FC = () => {
const navigate = useNavigate();
const { changePin } = useAuth();
const [step, setStep] = useState<'current' | 'new' | 'success'>('current');
const [currentPin, setCurrentPin] = useState('');
const [newPin, setNewPin] = useState('');
const [confirmPin, setConfirmPin] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleVerifyCurrent = (e: React.FormEvent) => {
e.preventDefault();
if (currentPin.length !== 6) {
setError('PIN must be 6 digits');
return;
}
setError(null);
setStep('new');
};
const handleChangePin = async (e: React.FormEvent) => {
e.preventDefault();
if (newPin.length !== 6) {
setError('New PIN must be 6 digits');
return;
}
if (newPin !== confirmPin) {
setError('New PINs do not match');
return;
}
if (newPin === currentPin) {
setError('New PIN cannot be the same as current PIN');
return;
}
setIsLoading(true);
setError(null);
const result = await changePin(currentPin, newPin);
setIsLoading(false);
if (result.success) {
setStep('success');
setTimeout(() => {
navigate('/profile');
}, 2000);
} else {
setError(result.message);
// If "incorrect current PIN", go back to step 1
if (result.message.toLowerCase().includes('current pin')) {
setStep('current');
setCurrentPin('');
}
}
};
return (
<div className="container change-pin-page">
<Header title="Change Security PIN" onBack={() => navigate('/profile')} showCart={false} />
<main className="change-pin-content">
<div className="change-pin-card animate-slideUp">
{step === 'current' && (
<>
<div className="step-indicator">Step 1 of 2</div>
<h2 className="step-title">Verify Identity</h2>
<p className="step-desc">Please enter your current 6-digit PIN to continue.</p>
<form onSubmit={handleVerifyCurrent} className="pin-form">
<div className="form-group">
<div className="input-wrapper">
<Lock size={18} className="input-icon" />
<input
type="password"
placeholder="Current PIN"
value={currentPin}
onChange={(e) => setCurrentPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
autoFocus
/>
</div>
</div>
{error && <div className="error-message">{error}</div>}
<button
className="primary-button"
type="submit"
disabled={currentPin.length !== 6}
>
Verify PIN
</button>
</form>
</>
)}
{step === 'new' && (
<>
<button className="back-link" onClick={() => setStep('current')}>
<ChevronLeft size={16} /> Back
</button>
<div className="step-indicator">Step 2 of 2</div>
<h2 className="step-title">Set New PIN</h2>
<p className="step-desc">Create a new 6-digit security PIN for your account.</p>
<form onSubmit={handleChangePin} className="pin-form">
<div className="form-group">
<label>New PIN</label>
<div className="input-wrapper">
<Lock size={18} className="input-icon" />
<input
type="password"
placeholder="Enter new 6-digit PIN"
value={newPin}
onChange={(e) => setNewPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
autoFocus
/>
</div>
</div>
<div className="form-group">
<label>Confirm New PIN</label>
<div className="input-wrapper">
<Lock size={18} className="input-icon" />
<input
type="password"
placeholder="Confirm new PIN"
value={confirmPin}
onChange={(e) => setConfirmPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
/>
</div>
</div>
{error && <div className="error-message">{error}</div>}
<button
className="primary-button"
type="submit"
disabled={isLoading || newPin.length !== 6 || confirmPin.length !== 6}
>
{isLoading ? 'Updating...' : 'Update Security PIN'}
</button>
</form>
</>
)}
{step === 'success' && (
<div className="success-state">
<CheckCircle2 size={64} className="success-check" />
<h2 className="step-title">PIN Updated!</h2>
<p className="step-desc">Your security PIN has been changed successfully. Redirecting you to profile...</p>
</div>
)}
</div>
</main>
</div>
);
};
export default ChangePinScreen;

View File

@@ -0,0 +1,186 @@
.checkout-page {
background-color: var(--bg);
}
.checkout-section {
background-color: var(--surface);
margin-bottom: 10px;
padding-bottom: 16px;
}
.address-options, .payment-options {
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.address-card, .payment-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 12px;
border: 1px solid var(--border);
cursor: pointer;
position: relative;
min-width: 0;
}
.address-card.selected, .payment-card.selected {
border-color: var(--primary);
background-color: var(--primary-light);
}
.address-icon, .payment-icon {
width: 40px;
height: 40px;
background-color: var(--bg);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-mid);
flex-shrink: 0;
}
.address-card.selected .address-icon, .payment-card.selected .payment-icon {
background-color: white;
color: var(--primary);
}
.address-info, .payment-info {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.address-type, .payment-name {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-dark);
}
.address-text, .payment-sub {
font-size: 0.75rem;
color: var(--text-mid);
line-height: 1.4;
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selection-radio {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.address-card.selected .selection-radio, .payment-card.selected .selection-radio {
border-color: var(--primary);
}
.radio-inner {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: transparent;
}
.address-card.selected .radio-inner, .payment-card.selected .radio-inner {
background-color: var(--primary);
}
.add-address {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
border: 1px dashed var(--primary);
border-radius: 12px;
color: var(--primary);
font-weight: 700;
font-size: 0.9rem;
margin-top: 4px;
}
.upi-placeholder {
font-weight: 800;
font-size: 0.7rem;
}
.order-summary-mini {
background-color: var(--surface);
padding: 16px;
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1rem;
font-weight: 600;
}
.section-subtitle {
font-size: 0.8rem;
color: var(--text-mid);
margin-top: 4px;
}
.upi-app-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.tax-info {
font-size: 0.75rem;
color: var(--text-mid);
margin-top: 8px;
text-align: right;
}
.summary-price {
font-weight: 800;
color: var(--primary);
}
.checkout-footer {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 600px;
background-color: var(--surface);
padding: 16px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
border-top: 1px solid var(--border);
z-index: 101;
}
.place-order-button {
width: 100%;
background-color: var(--primary);
color: white;
padding: 14px;
border-radius: 12px;
font-size: 1rem;
font-weight: 700;
}
@media (max-width: 360px) {
.address-card, .payment-card {
padding: 10px;
gap: 10px;
}
}

View File

@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Smartphone } from 'lucide-react';
import Header from '../components/Header';
import { useCart } from '../contexts/CartContext';
import { useAuth } from '../contexts/AuthContext';
import { useFood } from '../contexts/FoodContext';
import './CheckoutScreen.css';
const UPI_APPS = [
{ id: 'gpay', name: 'Google Pay', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-google-pay-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-3-pack-logos-icons-2944849.png' },
{ id: 'phonepe', name: 'PhonePe', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-phonepe-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-5-pack-logos-icons-2945037.png' },
{ id: 'paytm', name: 'Paytm', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-paytm-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-5-pack-logos-icons-2945031.png' },
{ id: 'bhim', name: 'BHIM UPI', icon: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-6R-pUu-Wj4V_vO8t-qXz9-7vjJ7XvK36A&s' },
{ id: 'other', name: 'Others', icon: null }
];
const CheckoutScreen: React.FC = () => {
const navigate = useNavigate();
const { cart, totalPrice, clearCart } = useCart();
const { user } = useAuth();
const { refreshData } = useFood();
const [selectedApp, setSelectedApp] = useState('gpay');
const [isProcessing, setIsProcessing] = useState(false);
const handlePlaceOrder = async () => {
if (!user) return;
setIsProcessing(true);
const orderData = {
userId: user.id,
totalAmount: totalPrice,
paymentMethod: UPI_APPS.find(a => a.id === selectedApp)?.name || 'UPI',
orderType: 'MY_ORDER',
items: cart.map(item => ({
productId: Number(item.id),
productName: item.name,
price: item.price,
quantity: item.quantity,
stallId: item.stallId ? Number(item.stallId) : null,
stallName: item.stallName || null
}))
};
console.warn('❗ DIAGNOSTIC: PRE-ORDER PAYLOAD CHECK');
console.table(orderData.items.map(i => ({ Name: i.productName, StallID: i.stallId, StallName: i.stallName })));
console.log('Full Payload:', JSON.stringify(orderData, null, 2));
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData),
});
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const data = await response.json();
if (data.success) {
await refreshData();
clearCart();
navigate('/success', {
state: {
orderNumber: data.orderNumber,
displayOrderId: data.displayOrderId
}
});
} else {
console.error('Order Logic Error:', data.message || data);
alert(data.message || 'Failed to place order');
}
} else {
const text = await response.text();
console.error('Order Server Error (Non-JSON):', text);
alert('Server Error. Check console for details.');
}
} catch (error) {
console.error('Order error:', error);
alert('Error connecting to server. Please try again.');
} finally {
setIsProcessing(false);
}
};
return (
<div className="container checkout-page">
<Header title="Checkout" showCart={false} />
<main className="safe-area-bottom">
<section className="checkout-section">
<div className="section-header">
<h2 className="section-title">Pay using UPI</h2>
<p className="section-subtitle">Select your preferred UPI app</p>
</div>
<div className="payment-options">
{UPI_APPS.map((app) => (
<div
key={app.id}
className={`payment-card ${selectedApp === app.id ? 'selected' : ''}`}
onClick={() => setSelectedApp(app.id)}
>
<div className="payment-icon">
{app.icon ? (
<img src={app.icon} alt={app.name} className="upi-app-icon" />
) : (
<div className="upi-placeholder">
<Smartphone size={20} />
</div>
)}
</div>
<div className="payment-info">
<span className="payment-name">{app.name}</span>
<span className="payment-sub">
{app.id === 'other' ? 'Pay via any installed UPI app' : `Fast & secure payments via ${app.name}`}
</span>
</div>
<div className="selection-radio">
<div className="radio-inner" />
</div>
</div>
))}
</div>
</section>
<div className="order-summary-mini">
<div className="summary-row">
<span>Amount to pay</span>
<span className="summary-price">{(totalPrice + 0).toFixed(2)}</span>
</div>
<p className="tax-info">Inclusive of all taxes and charges</p>
</div>
</main>
<div className="checkout-footer">
<button
className="place-order-button"
onClick={handlePlaceOrder}
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : `Pay ₹${totalPrice.toFixed(2)} & Place Order`}
</button>
</div>
</div>
);
};
export default CheckoutScreen;

View File

@@ -0,0 +1,275 @@
.welcome-section {
padding: 24px 16px 8px;
background-color: var(--surface);
}
.welcome-message {
font-size: 1.8rem;
font-weight: 800;
color: var(--text-dark);
line-height: 1.2;
}
.welcome-subtitle {
font-size: 0.95rem;
color: var(--text-mid);
margin-top: 4px;
}
.search-bar-container {
padding: 12px 16px;
background-color: var(--surface);
}
.search-bar {
display: flex;
align-items: center;
gap: 10px;
background-color: #f8f9ff;
padding: 10px 14px;
border-radius: 12px;
border: 1px solid var(--border);
}
.search-icon {
color: var(--text-light);
flex-shrink: 0;
}
.clear-search {
color: var(--text-light);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.search-bar input {
border: none;
background: none;
width: 100%;
font-size: 0.9rem;
color: var(--text-dark);
outline: none;
}
.section-header {
padding: 16px;
}
.section-title {
font-size: 1.1rem;
font-weight: 800;
color: var(--text-dark);
}
.categories-section {
background-color: var(--surface);
}
.categories-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 0 16px 16px;
}
@media (max-width: 360px) {
.categories-grid {
gap: 8px;
}
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
min-width: 0;
}
.category-image-wrapper {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary-light);
border: 1px solid var(--border);
}
.category-image-wrapper img[src=""] {
display: none;
}
.category-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.category-name {
font-size: 0.7rem;
font-weight: 600;
color: var(--text-mid);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.stalls-section {
background-color: var(--surface);
margin-top: 5px;
}
.stalls-carousel {
display: flex;
overflow-x: auto;
padding: 8px 16px 24px;
gap: 20px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.stalls-carousel::-webkit-scrollbar {
display: none;
}
.stall-card {
min-width: 280px;
max-width: 280px;
height: 200px;
border-radius: 24px;
overflow: hidden;
background-color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 10px 25px rgba(35, 22, 81, 0.08);
cursor: pointer;
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
position: relative;
}
.stall-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(35, 22, 81, 0.15);
}
.stall-card:active {
transform: scale(0.96);
}
.stall-card.closed {
filter: grayscale(1);
opacity: 0.9;
}
.stall-image-container {
height: 100%;
width: 100%;
position: relative;
overflow: hidden;
}
.stall-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.8s ease;
}
.stall-card:hover .stall-image {
transform: scale(1.15);
}
.stall-vignette {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.2) 50%,
rgba(0, 0, 0, 0.8) 100%
);
z-index: 1;
}
.stall-info-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px;
z-index: 2;
color: white;
}
.stall-name {
font-size: 1.2rem;
font-weight: 800;
color: #ffffff;
margin-bottom: 4px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.stall-desc {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.9);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.stall-closed-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(6px);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
letter-spacing: 3px;
font-size: 1.2rem;
z-index: 3;
}
.no-stalls {
padding: 16px;
color: var(--text-light);
font-style: italic;
font-size: 0.85rem;
}
.popular-section {
background-color: var(--surface);
margin-top: 10px;
flex: 1;
}
.items-list {
padding: 0;
}
.no-results {
padding: 40px 24px;
text-align: center;
color: var(--text-mid);
}

View File

@@ -0,0 +1,186 @@
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Search, RefreshCcw, X } from 'lucide-react';
import Header from '../components/Header';
import ItemCard from '../components/ItemCard';
import CartTab from '../components/CartTab';
import BottomNav from '../components/BottomNav';
import { useFood } from '../contexts/FoodContext';
import { useAuth } from '../contexts/AuthContext';
import './HomeScreen.css';
const HomeScreen: React.FC = () => {
const navigate = useNavigate();
const { categories, stalls, foodItems, isLoading, error, refreshData } = useFood();
const { user } = useAuth();
const [searchQuery, setSearchQuery] = useState('');
const popularItems = useMemo(() => foodItems.filter(item => item.isPopular), [foodItems]);
const filteredItems = useMemo(() => {
if (!searchQuery.trim()) return [];
const query = searchQuery.toLowerCase().trim();
return foodItems.filter(item =>
item.name.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query) ||
(item.description && item.description.toLowerCase().includes(query))
);
}, [foodItems, searchQuery]);
if (isLoading && categories.length === 0) {
return (
<div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div className="loading-spinner">Loading delicious food...</div>
</div>
);
}
if (error && categories.length === 0) {
return (
<div className="container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, textAlign: 'center' }}>
<h2 style={{ marginBottom: 12 }}>Oops! Something went wrong</h2>
<p style={{ color: 'var(--text-mid)', marginBottom: 24 }}>{error}</p>
<button className="primary-button" onClick={refreshData} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<RefreshCcw size={18} /> Try Again
</button>
</div>
);
}
return (
<div className="container">
<Header />
<main className="safe-area-bottom">
<div className="welcome-section">
<h1 className="welcome-message">Hello {user?.name || 'Guest'}!</h1>
<p className="welcome-subtitle">What would you like to eat today?</p>
</div>
<div className="search-bar-container">
<div className="search-bar">
<Search size={20} className="search-icon" />
<input
type="text"
placeholder="Search for food..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button onClick={() => setSearchQuery('')} className="clear-search">
<X size={18} />
</button>
)}
</div>
</div>
{searchQuery.trim() ? (
<section className="search-results-section">
<div className="section-header">
<h2 className="section-title">Search Results for "{searchQuery}"</h2>
</div>
<div className="items-list">
{filteredItems.map((item, index) => (
<ItemCard
key={item.id}
item={item}
isLast={index === filteredItems.length - 1}
/>
))}
{filteredItems.length === 0 && (
<div className="no-results">
<p>No items found matching your search.</p>
</div>
)}
</div>
</section>
) : (
<>
<section className="categories-section">
<div className="section-header">
<h2 className="section-title">Categories</h2>
</div>
<div className="categories-grid">
{categories.map((category) => (
<div
key={category.id}
className="category-item"
onClick={() => navigate(`/category/${category.name}`)}
>
<div className="category-image-wrapper" style={{ backgroundColor: category.color }}>
<span style={{ fontSize: 32 }}>{category.emoji}</span>
</div>
<span className="category-name">{category.name}</span>
</div>
))}
</div>
{categories.length === 0 && !isLoading && (
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-light)' }}>
No categories found.
</div>
)}
</section>
<section className="stalls-section">
<div className="section-header">
<h2 className="section-title">Our Favorite Stalls</h2>
</div>
<div className="stalls-carousel">
{stalls.map((stall) => (
<div
key={stall.id}
className={`stall-card ${stall.temporarilyClosed ? 'closed' : ''}`}
onClick={() => navigate(`/stall/${stall.id}`)}
>
<div className="stall-image-container">
{stall.imageData ? (
<img src={stall.imageData} alt={stall.name} className="stall-image" />
) : (
<div className="header-placeholder" style={{ width: '100%', height: '100%' }} />
)}
<div className="stall-vignette" />
<div className="stall-info-overlay">
<h3 className="stall-name">{stall.name}</h3>
<p className="stall-desc">{stall.description}</p>
</div>
{stall.temporarilyClosed && (
<div className="stall-closed-overlay">CLOSED</div>
)}
</div>
</div>
))}
</div>
{stalls.length === 0 && !isLoading && (
<div className="no-stalls">No stalls available.</div>
)}
</section>
<section className="popular-section">
<div className="section-header">
<h2 className="section-title">Popular Items</h2>
</div>
<div className="items-list">
{popularItems.map((item, index) => (
<ItemCard
key={item.id}
item={item}
isLast={index === popularItems.length - 1}
/>
))}
{popularItems.length === 0 && !isLoading && (
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-light)' }}>
No popular items at the moment.
</div>
)}
</div>
</section>
</>
)}
</main>
<CartTab />
<BottomNav />
</div>
);
};
export default HomeScreen;

View File

@@ -0,0 +1,261 @@
.item-detail-page {
background-color: var(--surface);
}
.item-detail-page.out-of-stock {
opacity: 0.8;
}
.item-detail-page.out-of-stock .item-hero {
filter: grayscale(0.5);
}
.out-of-stock-badge {
background-color: #f3f4f6;
color: #6b7280;
font-size: 0.65rem;
font-weight: 700;
padding: 1px 6px;
border-radius: 20px;
white-space: nowrap;
}
.item-hero {
width: 100%;
aspect-ratio: 16/9;
max-height: 300px;
overflow: hidden;
}
.item-hero-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.item-hero-placeholder {
width: 100%;
height: 100%;
aspect-ratio: 16/9;
max-height: 300px;
background: linear-gradient(135deg, var(--primary-light), #fff) ;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.item-hero-placeholder::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: radial-gradient(var(--primary) 0.5px, transparent 0.5px);
background-size: 15px 15px;
opacity: 0.1;
}
.placeholder-emoji {
font-size: 4rem;
filter: drop-shadow(0 4px 10px rgba(0,0,0,0.1));
animation: float 3s ease-in-out infinite;
z-index: 1;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
.item-details-content {
padding: 20px 16px;
}
.item-header-info {
margin-bottom: 20px;
}
.item-name-large {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-dark);
margin: 8px 0;
line-height: 1.2;
}
.item-price-large {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary);
}
.item-stats {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--bg);
padding: 12px;
border-radius: 12px;
margin-bottom: 20px;
gap: 8px;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
justify-content: center;
}
.stat-info {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 0.85rem;
font-weight: 700;
color: var(--text-dark);
white-space: nowrap;
}
.stat-label {
font-size: 0.65rem;
color: var(--text-mid);
}
.stat-divider {
width: 1px;
height: 20px;
background-color: var(--border);
}
.section-title {
font-size: 1.1rem;
font-weight: 800;
color: var(--text-dark);
margin-bottom: 8px;
}
.item-long-description {
font-size: 0.9rem;
line-height: 1.5;
color: var(--text-mid);
margin-bottom: 20px;
}
.item-extra-info {
display: flex;
flex-direction: column;
gap: 10px;
border-top: 1px solid var(--border);
padding-top: 20px;
}
.info-row {
display: flex;
justify-content: space-between;
gap: 12px;
}
.info-label {
font-size: 0.85rem;
color: var(--text-mid);
}
.info-value {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-dark);
text-align: right;
}
.info-value.green { color: var(--green); }
.info-value.red { color: var(--red); }
.item-footer {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 600px;
background-color: var(--surface);
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
border-top: 1px solid var(--border);
z-index: 101;
gap: 16px;
}
.footer-price-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.total-label {
font-size: 0.75rem;
color: var(--text-mid);
}
.total-value {
font-size: 1.1rem;
font-weight: 800;
color: var(--text-dark);
}
.primary-action-button {
background-color: var(--primary);
color: white;
padding: 12px 24px;
border-radius: 12px;
font-size: 1rem;
font-weight: 700;
flex: 1;
max-width: 200px;
text-align: center;
}
.footer-quantity-controls {
display: flex;
align-items: center;
gap: 16px;
background-color: var(--primary-light);
padding: 8px 16px;
border-radius: 12px;
flex-shrink: 0;
}
.footer-quantity-controls button {
font-size: 1.5rem;
font-weight: 800;
color: var(--primary);
display: flex;
align-items: center;
}
.footer-quantity-controls .quantity {
font-size: 1.1rem;
font-weight: 800;
color: var(--primary);
min-width: 24px;
text-align: center;
}
@media (max-width: 360px) {
.item-footer {
padding: 10px 12px;
}
.primary-action-button {
padding: 10px 16px;
font-size: 0.9rem;
}
}

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import Header from '../components/Header';
import { useCart } from '../contexts/CartContext';
import { useFood } from '../contexts/FoodContext';
import CartTab from '../components/CartTab';
import './ItemDetailScreen.css';
const ItemDetailScreen: React.FC = () => {
const { itemId } = useParams<{ itemId: string }>();
const { addToCart, updateQuantity, getItemQuantity } = useCart();
const { foodItems, isLoading } = useFood();
const item = foodItems.find((i) => i.id === itemId);
const quantity = item ? getItemQuantity(item.id) : 0;
if (isLoading && foodItems.length === 0) {
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
}
if (!item && !isLoading) return <div className="container" style={{ padding: 24 }}>Item not found</div>;
if (!item) return null;
return (
<div className={`container item-detail-page ${item.stock === 0 ? 'out-of-stock' : ''}`}>
<Header title="" />
<main className="safe-area-bottom">
<div className="item-hero">
{item.image ? (
<img src={item.image} alt={item.name} className="item-hero-image" />
) : (
<div className="item-hero-placeholder">
<span className="placeholder-emoji">🍲</span>
</div>
)}
</div>
<div className="item-details-content">
<div className="item-header-info">
<div className="item-labels">
<div className={`veg-icon ${item.isVeg ? 'veg' : 'non-veg'}`}>
<div className="dot" />
</div>
{item.isPopular && <span className="bestseller-badge">Popular</span>}
{item.stock === 0 && <span className="out-of-stock-badge">Sold Out</span>}
</div>
<h1 className="item-name-large">{item.name}</h1>
<p className="item-price-large">{item.price.toFixed(2)}</p>
</div>
<div className="item-description-section">
<h2 className="section-title">Description</h2>
<p className="item-long-description">
{item.longDescription || item.description}
</p>
</div>
<div className="item-extra-info">
<div className="info-row">
<span className="info-label">Category</span>
<span className="info-value">{item.category}</span>
</div>
<div className="info-row">
<span className="info-label">Dietary</span>
<span className="info-value">{item.isVeg ? 'Vegetarian' : 'Non-Vegetarian'}</span>
</div>
<div className="info-row">
<span className="info-label">Availability</span>
<span className={`info-value ${item.stock && item.stock > 0 ? 'green' : 'red'}`}>
{item.stock && item.stock > 0 ? 'In Stock' : 'Out of Stock'}
</span>
</div>
</div>
</div>
</main>
<footer className="item-footer">
<div className="footer-price-info">
<span className="total-label">Price</span>
<span className="total-value">{(item.price * Math.max(1, quantity)).toFixed(2)}</span>
</div>
<div className="footer-action">
{quantity === 0 ? (
<button
className="primary-action-button"
onClick={() => addToCart(item)}
disabled={item.stock === 0}
style={{ opacity: item.stock === 0 ? 0.5 : 1 }}
>
{item.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
</button>
) : (
<div className="footer-quantity-controls">
<button onClick={() => updateQuantity(item.id, -1)}></button>
<span className="quantity">{quantity}</span>
<button
onClick={() => addToCart(item)}
disabled={item.stock !== undefined && quantity >= item.stock}
>+</button>
</div>
)}
</div>
</footer>
<CartTab />
</div>
);
};
export default ItemDetailScreen;

View File

@@ -0,0 +1,248 @@
.login-page {
position: relative;
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: #f8f9ff;
}
/* Modern Mesh Gradient Background */
.mesh-gradient-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
background-color: #ffffff;
background-image:
radial-gradient(at 0% 0%, rgba(35, 22, 81, 0.15) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(0, 208, 148, 0.1) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(35, 22, 81, 0.1) 0px, transparent 50%),
radial-gradient(at 0% 100%, rgba(0, 208, 148, 0.15) 0px, transparent 50%);
}
.login-content {
position: relative;
z-index: 1;
width: 100%;
max-width: 420px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* Glassmorphism Card */
.login-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 32px;
padding: 40px 32px;
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow:
0 20px 50px rgba(35, 22, 81, 0.1),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}
.login-header-logo {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.rit-logo {
height: 80px;
width: auto;
object-fit: contain;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1));
}
.login-header {
position: relative;
text-align: center;
margin-bottom: 32px;
}
.step-back-btn {
position: absolute;
left: -8px;
top: 4px;
color: var(--text-mid);
padding: 8px;
border-radius: 50%;
transition: all 0.2s ease;
}
.step-back-btn:hover {
background-color: var(--bg);
color: var(--primary);
}
.login-title {
font-size: 26px;
font-weight: 850;
color: var(--text-dark);
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.login-subtitle {
font-size: 14px;
color: var(--text-mid);
font-weight: 500;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 13px;
font-weight: 700;
color: var(--text-mid);
margin-left: 4px;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.input-icon {
position: absolute;
left: 18px;
color: var(--text-light);
transition: color 0.3s ease;
}
.login-form input {
width: 100%;
padding: 16px 16px 16px 52px;
border-radius: 16px;
border: 2px solid transparent;
background-color: #f0f2f5;
font-size: 16px;
font-weight: 600;
color: var(--text-dark);
outline: none;
transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.login-form input:focus {
background-color: #ffffff;
border-color: var(--primary-light);
box-shadow: 0 0 0 4px rgba(35, 22, 81, 0.05);
}
.login-form input:focus + .input-icon {
color: var(--primary);
}
.login-button {
background-color: var(--primary);
color: white;
padding: 18px;
border-radius: 18px;
font-size: 16px;
font-weight: 800;
margin-top: 12px;
box-shadow: 0 10px 25px rgba(35, 22, 81, 0.25);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
align-items: center;
justify-content: center;
min-height: 58px;
}
.login-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 15px 30px rgba(35, 22, 81, 0.3);
background-color: #2b1c63;
}
.login-button:active:not(:disabled) {
transform: translateY(0);
}
.login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
box-shadow: none;
}
.loading-dots span {
animation: loadingDots 1.4s infinite;
display: inline-block;
font-size: 24px;
line-height: 0;
margin: 0 2px;
}
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes loadingDots {
0%, 80%, 100% { transform: translateY(0); opacity: 0.2; }
40% { transform: translateY(-4px); opacity: 1; }
}
.error-message {
color: #ff4d4f;
font-size: 13px;
font-weight: 600;
background-color: rgba(255, 77, 79, 0.1);
padding: 12px 16px;
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid rgba(255, 77, 79, 0.2);
text-align: center;
}
.back-to-mobile {
color: var(--primary);
font-weight: 700;
font-size: 14px;
margin-top: 8px;
text-align: center;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s;
}
.back-to-mobile:hover {
background-color: var(--primary-light);
}
.login-footer-info {
text-align: center;
display: flex;
flex-direction: column;
gap: 4px;
}
.login-footer-info p {
font-size: 12px;
color: var(--text-light);
font-weight: 600;
}
@media (max-width: 480px) {
.login-card {
padding: 32px 24px;
border-radius: 24px;
}
}

View File

@@ -0,0 +1,222 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Phone, User as UserIcon, Lock, ChevronLeft } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import ritLogo from '../assets/RIT_Logo.png';
import './LoginScreen.css';
const LoginScreen: React.FC = () => {
const navigate = useNavigate();
const { checkUserExists, login, register } = useAuth();
const [step, setStep] = useState<'mobile' | 'pin' | 'register'>('mobile');
const [mobileNumber, setMobileNumber] = useState('');
const [name, setName] = useState('');
const [pin, setPin] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleMobileSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (mobileNumber.length !== 10) {
setError('Please enter a valid 10-digit mobile number');
return;
}
setIsLoading(true);
setError(null);
const result = await checkUserExists(mobileNumber);
setIsLoading(false);
if (result.success) {
if (result.userExists) {
setStep('pin');
} else {
setStep('register');
}
} else {
setError(result.message);
}
};
const handleLoginSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (pin.length !== 6) {
setError('PIN must be 6 digits');
return;
}
setIsLoading(true);
setError(null);
const result = await login(mobileNumber, pin);
setIsLoading(false);
if (result.success) {
navigate('/');
} else {
setError(result.message);
}
};
const handleRegisterSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (name.length < 2) {
setError('Please enter a valid name');
return;
}
if (pin.length !== 6) {
setError('PIN must be 6 digits');
return;
}
setIsLoading(true);
setError(null);
const result = await register(mobileNumber, name, pin);
setIsLoading(false);
if (result.success) {
navigate('/');
} else {
setError(result.message);
}
};
return (
<div className="container login-page">
<div className="mesh-gradient-bg"></div>
<div className="login-content">
<div className="login-card animate-slideUp">
<div className="login-header-logo">
<img src={ritLogo} alt="RIT Logo" className="rit-logo" />
</div>
<div className="login-header">
{step !== 'mobile' && (
<button
className="step-back-btn"
onClick={() => setStep('mobile')}
disabled={isLoading}
>
<ChevronLeft size={20} />
</button>
)}
<h1 className="login-title">
{step === 'mobile' ? 'Welcome!' : step === 'pin' ? 'Welcome Back' : 'Create Account'}
</h1>
<p className="login-subtitle">
{step === 'mobile'
? 'Enter your mobile number to get started'
: step === 'pin'
? `Enter PIN for ${mobileNumber}`
: 'Enter your details to register'}
</p>
</div>
{error && <div className="error-message">{error}</div>}
{step === 'mobile' && (
<form className="login-form" onSubmit={handleMobileSubmit}>
<div className="form-group">
<label>Mobile Number</label>
<div className="input-wrapper">
<Phone size={18} className="input-icon" />
<input
type="tel"
placeholder="10-digit mobile number"
value={mobileNumber}
onChange={(e) => setMobileNumber(e.target.value.replace(/[^0-9]/g, '').slice(0, 10))}
disabled={isLoading}
/>
</div>
</div>
<button className="login-button" type="submit" disabled={isLoading || mobileNumber.length !== 10}>
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Continue'}
</button>
</form>
)}
{step === 'pin' && (
<form className="login-form" onSubmit={handleLoginSubmit}>
<div className="form-group">
<label>Security PIN</label>
<div className="input-wrapper">
<Lock size={18} className="input-icon" />
<input
type="password"
placeholder="Enter 6-digit PIN"
value={pin}
onChange={(e) => setPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
disabled={isLoading}
autoFocus
/>
</div>
</div>
<button className="login-button" type="submit" disabled={isLoading || pin.length !== 6}>
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Verify & Login'}
</button>
<button
className="back-to-mobile"
type="button"
onClick={() => setStep('mobile')}
disabled={isLoading}
>
Use different mobile number
</button>
</form>
)}
{step === 'register' && (
<form className="login-form" onSubmit={handleRegisterSubmit}>
<div className="form-group">
<label>Full Name</label>
<div className="input-wrapper">
<UserIcon size={18} className="input-icon" />
<input
type="text"
placeholder="What should we call you?"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isLoading}
autoFocus
/>
</div>
</div>
<div className="form-group">
<label>Create PIN</label>
<div className="input-wrapper">
<Lock size={18} className="input-icon" />
<input
type="password"
placeholder="Choose a 6-digit PIN"
value={pin}
onChange={(e) => setPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
disabled={isLoading}
/>
</div>
</div>
<button className="login-button" type="submit" disabled={isLoading || name.length < 2 || pin.length !== 6}>
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Complete Registration'}
</button>
<button
className="back-to-mobile"
type="button"
onClick={() => setStep('mobile')}
disabled={isLoading}
>
Cancel
</button>
</form>
)}
</div>
<div className="login-footer-info">
<p>© 2026 Rajalakshmi Institute of Technology</p>
<p>Canteen Automation System</p>
</div>
</div>
</div>
);
};
export default LoginScreen;

View File

@@ -0,0 +1,321 @@
.orders-page {
background-color: var(--bg);
}
.orders-content {
padding: 16px;
padding-bottom: 90px;
}
.home-content {
padding: 16px;
padding-bottom: 90px;
}
.section-label {
font-size: 13px;
font-weight: 700;
color: var(--text-mid);
text-transform: uppercase;
margin-bottom: 12px;
letter-spacing: 0.5px;
}
.latest-order-section {
margin-bottom: 32px;
}
.latest-order-card {
background: white;
border-radius: 24px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 16px;
cursor: pointer;
transition: transform 0.2s ease;
}
.latest-order-card:active {
transform: scale(0.98);
}
.latest-order-info {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.order-main-info {
display: flex;
flex-direction: column;
}
.order-number {
font-size: 18px;
font-weight: 800;
color: var(--text-dark);
}
.order-date {
font-size: 12px;
color: var(--text-mid);
}
.order-status-badge {
background: var(--green-light);
color: var(--green);
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
}
.latest-qr-wrapper {
background: var(--bg);
padding: 16px;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.mini-qr {
max-width: 100%;
}
.qr-hint-text {
font-size: 11px;
color: var(--text-mid);
font-weight: 500;
}
.order-items-summary {
font-size: 14px;
color: var(--text-dark);
line-height: 1.4;
padding-bottom: 12px;
border-bottom: 1px dashed var(--border);
}
.order-total-bar {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 15px;
font-weight: 600;
color: var(--text-mid);
}
.amount-text {
font-size: 18px;
font-weight: 800;
color: var(--primary);
}
/* Previous Orders List */
.previous-orders-section {
display: flex;
flex-direction: column;
}
.orders-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.order-item-list {
background: white;
padding: 14px;
border-radius: 16px;
display: flex;
align-items: center;
gap: 12px;
border: 1px solid var(--border);
cursor: pointer;
}
.order-icon-bg {
width: 40px;
height: 40px;
background: var(--bg);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-mid);
}
.order-list-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.order-list-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-list-number {
font-size: 14px;
font-weight: 700;
color: var(--text-dark);
}
.order-list-price {
font-size: 14px;
font-weight: 700;
color: var(--primary);
}
.order-list-bottom {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: var(--text-mid);
}
.list-chevron {
color: var(--text-light);
}
/* Modal / Overlay */
.order-detail-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 2000;
display: flex;
align-items: flex-end;
}
.order-detail-modal {
width: 100%;
background: white;
border-radius: 24px 24px 0 0;
padding: 24px;
display: flex;
flex-direction: column;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.close-details {
position: absolute;
top: 16px;
right: 16px;
color: var(--text-mid);
}
.modal-header {
margin-bottom: 24px;
}
.modal-header h3 {
font-size: 20px;
font-weight: 800;
}
.modal-header p {
font-size: 12px;
color: var(--primary);
font-weight: 600;
}
.modal-qr-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-bottom: 32px;
padding: 24px;
background: var(--bg);
border-radius: 20px;
}
.modal-qr-hint {
font-size: 13px;
color: var(--text-mid);
font-weight: 500;
}
.modal-info-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 14px;
}
.info-item .label { color: var(--text-mid); }
.info-item .value { font-weight: 600; color: var(--text-dark); }
.info-item .status-completed { color: var(--green); }
.modal-items-section {
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid var(--border);
padding-top: 20px;
}
.modal-items-section h4 {
font-size: 15px;
margin-bottom: 4px;
}
.modal-item-row {
display: flex;
justify-content: space-between;
font-size: 14px;
color: var(--text-mid);
}
.modal-total-row {
display: flex;
justify-content: space-between;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bg);
font-size: 16px;
font-weight: 800;
color: var(--primary);
}
.empty-orders {
padding: 80px 24px;
display: flex;
flex-direction: column;
align-items: center;
color: var(--text-light);
text-align: center;
}
.empty-icon {
margin-bottom: 20px;
opacity: 0.5;
}

View File

@@ -0,0 +1,220 @@
import React, { useState, useEffect } from 'react';
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X } from 'lucide-react';
import { QRCodeCanvas } from 'qrcode.react';
import Header from '../components/Header';
import BottomNav from '../components/BottomNav';
import { useAuth } from '../contexts/AuthContext';
import './MyOrdersScreen.css';
interface Order {
id: number;
orderNumber: string;
displayOrderId: string;
totalAmount: number;
status: string;
paymentMethod: string;
createdAt: string;
archived: boolean;
items: Array<{
productName: string;
quantity: number;
price: number;
}>;
}
const MyOrdersScreen: React.FC = () => {
const { user } = useAuth();
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
useEffect(() => {
if (user?.id) {
fetchOrders();
}
}, [user]);
const fetchOrders = async () => {
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`);
const data = await response.json();
setOrders(data);
} catch (error) {
console.error('Error fetching orders:', error);
} finally {
setLoading(false);
}
};
const latestOrder = orders.length > 0 ? orders[0] : null;
const previousOrders = orders.length > 1 ? orders.slice(1) : [];
if (loading) {
return <div className="container">Loading your orders...</div>;
}
return (
<div className="container orders-page">
<Header title="My Orders" showCart={false} />
<main className="orders-content safe-area-bottom">
{orders.length === 0 ? (
<div className="empty-orders">
<ShoppingBagIcon size={64} className="empty-icon" />
<h3>No orders yet</h3>
<p>Hungry? Place your first order now!</p>
</div>
) : (
<>
{/* Latest Order Section */}
{latestOrder && (
<section className="latest-order-section">
<div className="section-label">Active/Latest Order</div>
<div className="latest-order-card" onClick={() => setSelectedOrder(latestOrder)}>
<div className="latest-order-info">
<div className="order-main-info">
<span className="order-number">Order #{latestOrder.displayOrderId || latestOrder.id}</span>
<span className="order-date">{new Date(latestOrder.createdAt).toLocaleDateString()}</span>
</div>
<div className="order-status-badge">{latestOrder.status}</div>
</div>
<div className={`latest-qr-wrapper ${latestOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${latestOrder.archived ? 'qr-expired' : ''}`}>
<div className="qr-container">
<QRCodeCanvas value={latestOrder.orderNumber} size={120} className="mini-qr" />
{latestOrder.status.toUpperCase() === 'COMPLETED' && (
<div className="qr-overlay mini">
<span className="overlay-text mini">COMPLETED</span>
</div>
)}
{latestOrder.archived && (
<div className="qr-overlay mini">
<span className="overlay-text mini expired">EXPIRED</span>
</div>
)}
</div>
<div className="qr-hint-text">
{latestOrder.status.toUpperCase() === 'COMPLETED' ? 'Order Fulfilled' : 'Tap to enlarge QR'}
</div>
</div>
<div className="order-items-summary">
{latestOrder.items.map((item, idx) => (
<span key={idx}>{item.quantity}x {item.productName}{idx < latestOrder.items.length - 1 ? ', ' : ''}</span>
))}
</div>
<div className="order-total-bar">
<span>Total Amount</span>
<span className="amount-text">{latestOrder.totalAmount.toFixed(2)}</span>
</div>
</div>
</section>
)}
{/* Previous Orders List */}
{previousOrders.length > 0 && (
<section className="previous-orders-section">
<div className="section-label">Previous Orders</div>
<div className="orders-list">
{previousOrders.map((order) => (
<div key={order.id} className="order-item-list" onClick={() => setSelectedOrder(order)}>
<div className="order-icon-bg">
<Clock size={20} />
</div>
<div className="order-list-info">
<div className="order-list-top">
<span className="order-list-number">#{order.displayOrderId}</span>
<span className="order-list-price">{order.totalAmount.toFixed(0)}</span>
</div>
<div className="order-list-bottom">
<span className="order-list-date">{new Date(order.createdAt).toLocaleDateString()}</span>
<span className="order-list-items">{order.items.length} items</span>
</div>
</div>
<ChevronRightIcon size={20} className="list-chevron" />
</div>
))}
</div>
</section>
)}
</>
)}
</main>
{/* Order Detail Modal / Overlay */}
{selectedOrder && (
<div className="order-detail-overlay" onClick={() => setSelectedOrder(null)}>
<div className="order-detail-modal" onClick={(e) => e.stopPropagation()}>
<button className="close-details" onClick={() => setSelectedOrder(null)}>
<X size={24} />
</button>
<div className="modal-header">
<h3>Order Details</h3>
<p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p>
</div>
<div className={`modal-qr-section ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${selectedOrder.archived ? 'qr-expired' : ''}`}>
<div className="qr-container">
<QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} />
{selectedOrder.status.toUpperCase() === 'COMPLETED' && (
<div className="qr-overlay">
<span className="overlay-text">COMPLETED</span>
</div>
)}
{selectedOrder.archived && (
<div className="qr-overlay">
<span className="overlay-text expired">EXPIRED</span>
</div>
)}
</div>
<p className="modal-qr-hint">
{selectedOrder.archived
? 'This order has expired'
: selectedOrder.status.toUpperCase() === 'COMPLETED'
? 'This order has been fulfilled'
: 'Show this QR code at the counter'}
</p>
</div>
<div className="modal-info-list">
<div className="info-item">
<span className="label">Date</span>
<span className="value">{new Date(selectedOrder.createdAt).toLocaleString()}</span>
</div>
<div className="info-item">
<span className="label">Payment</span>
<span className="value">{selectedOrder.paymentMethod}</span>
</div>
<div className="info-item">
<span className="label">Status</span>
<span className={`value status-badge ${selectedOrder.status.toLowerCase()}`}>
{selectedOrder.status}
</span>
</div>
</div>
<div className="modal-items-section">
<h4>Items Ordered</h4>
{selectedOrder.items.map((item, idx) => (
<div key={idx} className="modal-item-row">
<span>{item.quantity} x {item.productName}</span>
<span>{(item.price * item.quantity).toFixed(0)}</span>
</div>
))}
<div className="modal-total-row">
<span>Grand Total</span>
<span>{selectedOrder.totalAmount.toFixed(2)}</span>
</div>
</div>
</div>
</div>
)}
<BottomNav />
</div>
);
};
export default MyOrdersScreen;

View File

@@ -0,0 +1,207 @@
.profile-page {
background-color: var(--bg);
min-height: 100vh;
}
.profile-content {
padding: 16px;
padding-bottom: 50px;
}
/* User Profile Card */
.profile-card-section {
padding: 16px 0;
}
.profile-card {
background: var(--surface);
border-radius: 20px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 20px rgba(35, 22, 81, 0.08);
border: 1px solid var(--border);
position: relative;
}
.profile-avatar-wrapper {
flex-shrink: 0;
}
.profile-avatar {
width: 64px;
height: 64px;
background-color: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 800;
border: 3px solid var(--primary-light);
}
.profile-info {
flex: 1;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 1.25rem;
font-weight: 800;
color: var(--text-dark);
}
.user-contact {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 4px;
}
.contact-item {
font-size: 0.85rem;
color: var(--text-mid);
display: flex;
align-items: center;
gap: 6px;
}
.edit-btn {
position: absolute;
top: 16px;
right: 16px;
padding: 6px 12px;
background: var(--primary-light);
color: var(--primary);
border-radius: 12px;
font-size: 0.75rem;
font-weight: 700;
}
/* Stats Section */
.profile-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.stat-box {
background: var(--surface);
padding: 16px;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
cursor: pointer;
transition: transform 0.2s ease;
}
.stat-box:active {
transform: scale(0.95);
}
.stat-value {
font-size: 1.1rem;
font-weight: 800;
color: var(--primary);
}
.stat-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-light);
text-transform: uppercase;
}
/* Menu Section */
.profile-menu {
background: var(--surface);
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--border);
margin-bottom: 24px;
}
.menu-item {
display: flex;
align-items: center;
padding: 16px;
gap: 16px;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid var(--border);
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: var(--primary-light);
}
.menu-icon-bg {
width: 40px;
height: 40px;
background-color: var(--primary-light);
color: var(--primary);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.menu-text {
flex: 1;
display: flex;
flex-direction: column;
}
.menu-label {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-dark);
}
.menu-sub {
font-size: 0.75rem;
color: var(--text-mid);
}
.menu-chevron {
color: var(--text-light);
}
/* Logout Section */
.logout-section {
padding: 16px;
}
.logout-button {
width: 100%;
padding: 16px;
background-color: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
border-radius: 16px;
font-size: 1rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border: 1px solid rgba(255, 77, 79, 0.2);
}
.app-version {
text-align: center;
margin-top: 16px;
font-size: 0.75rem;
color: var(--text-light);
}

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
ShoppingBag,
Settings,
HelpCircle,
LogOut,
ChevronRight,
Phone,
ShieldCheck
} from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import Header from '../components/Header';
import BottomNav from '../components/BottomNav';
import './ProfileScreen.css';
const ProfileScreen: React.FC = () => {
const navigate = useNavigate();
const { user, logout } = useAuth();
const handleLogout = () => {
if (window.confirm('Are you sure you want to logout?')) {
logout();
navigate('/login');
}
};
const menuItems = [
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
{ icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' },
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
];
return (
<div className="container profile-page">
<Header title="My Profile" showCart={false} />
<main className="profile-content safe-area-bottom">
{/* User Profile Card */}
<section className="profile-card-section">
<div className="profile-card">
<div className="profile-avatar-wrapper">
<div className="profile-avatar">
{user?.name?.charAt(0).toUpperCase() || 'U'}
</div>
</div>
<div className="profile-info">
<h2 className="user-name">{user?.name || 'User Name'}</h2>
<div className="user-contact">
<span className="contact-item"><Phone size={14} /> {user?.mobileNumber || '+91 XXXXX XXXXX'}</span>
{/* <span className="contact-item"><Mail size={14} /> user@example.com</span> */}
</div>
</div>
<button className="edit-btn">Edit</button>
</div>
</section>
{/* Stats Section */}
<section className="profile-stats">
<div className="stat-box" onClick={() => navigate('/orders')}>
<span className="stat-value">--</span>
<span className="stat-label">Orders</span>
</div>
<div className="stat-box">
<span className="stat-value">0</span>
<span className="stat-label">Spent</span>
</div>
<div className="stat-box">
<span className="stat-value">0</span>
<span className="stat-label">Coins</span>
</div>
</section>
{/* Menu Section */}
<section className="profile-menu">
{menuItems.map((item, index) => (
<div key={index} className="menu-item" onClick={() => item.path !== '#' && navigate(item.path)}>
<div className="menu-icon-bg">
<item.icon size={20} />
</div>
<div className="menu-text">
<span className="menu-label">{item.label}</span>
<span className="menu-sub">{item.sub}</span>
</div>
<ChevronRight size={20} className="menu-chevron" />
</div>
))}
</section>
{/* Logout Button */}
<section className="logout-section">
<button className="logout-button" onClick={handleLogout}>
<LogOut size={20} />
<span>Logout</span>
</button>
<p className="app-version">Version 1.0.4 (Stable)</p>
</section>
</main>
<BottomNav />
</div>
);
};
export default ProfileScreen;

View File

@@ -0,0 +1,166 @@
.stall-detail-page {
background-color: #f7f8fc;
min-height: 100vh;
}
.detail-header {
position: relative;
height: 220px;
width: 100%;
}
.detail-header.sticky {
height: auto;
padding: 12px 16px;
background: white;
z-index: 100;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.stall-detail-page .back-button {
position: absolute;
top: 16px;
left: 16px;
z-index: 10;
background: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.header-image-container {
width: 100%;
height: 100%;
position: relative;
}
.header-bg-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.header-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f0f1ff 0%, #e5e7ff 100%);
}
.header-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0) 40%, rgba(247,248,252,1) 100%);
}
.stall-main-card {
margin: -40px 16px 20px;
background: white;
border-radius: 24px;
padding: 24px;
position: relative;
box-shadow: 0 8px 30px rgba(35, 22, 81, 0.08);
z-index: 5;
}
.stall-header-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.stall-title {
font-size: 1.6rem;
font-weight: 800;
color: var(--text-dark);
}
.status-tag {
padding: 4px 10px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 700;
}
.status-tag.closed {
background-color: #fee2e2;
color: #ef4444;
}
.stall-description {
font-size: 0.95rem;
color: var(--text-mid);
line-height: 1.5;
margin-bottom: 20px;
}
.stall-meta {
display: flex;
gap: 16px;
border-top: 1px solid #f0f1ff;
padding-top: 16px;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
font-weight: 600;
color: var(--primary);
background: var(--primary-light);
padding: 6px 12px;
border-radius: 100px;
}
.stall-items-section {
padding: 10px 0;
}
.stall-items-section .section-title {
padding: 0 20px 16px;
font-size: 1.2rem;
font-weight: 800;
color: var(--text-dark);
}
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 40px;
}
.error-content {
margin-top: 60px;
}
.error-content h2 {
margin-bottom: 12px;
color: var(--text-dark);
}
.error-content p {
color: var(--text-mid);
margin-bottom: 24px;
}
.no-items {
padding: 40px 24px;
text-align: center;
color: var(--text-light);
background: white;
margin: 0 16px;
border-radius: 16px;
border: 1px dashed var(--border);
}

View File

@@ -0,0 +1,178 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Clock, Info } from 'lucide-react';
import ItemCard from '../components/ItemCard';
import CartTab from '../components/CartTab';
import BottomNav from '../components/BottomNav';
import type { Stall } from '../types';
import './StallDetailScreen.css';
const StallDetailScreen: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [stall, setStall] = useState<Stall | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStallDetails = async () => {
setLoading(true);
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/stalls/${id}`);
if (!response.ok) throw new Error('Stall not found');
const data = await response.json();
// Map backend stall to frontend Stall type
const mappedStall: Stall = {
id: data.id.toString(),
name: data.name,
description: data.description || 'Quality food prepared with care',
imageData: data.imageData?.trim() ? (data.imageData.trim().startsWith('data:') ? data.imageData.trim() : `data:image/png;base64,${data.imageData.trim()}`) : '',
active: data.active,
temporarilyClosed: data.temporarilyClosed,
products: data.products.map((p: any) => {
const rawImg = p.imageData?.trim();
const finalImage = rawImg ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) : '';
return {
id: p.id.toString(),
name: p.name,
description: p.description || 'Delicious meal item',
price: p.price || p.basePrice || 0,
category: p.category,
image: finalImage,
isVeg: p.veg,
isPopular: p.active,
stock: p.stock,
stallId: data.id.toString(),
stallName: data.name
};
})
};
setStall(mappedStall);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchStallDetails();
// SSE Real-time stock updates
const host = window.location.hostname;
const eventSource = new EventSource(`http://${host}:8080/api/stock/stream`);
eventSource.addEventListener('stockUpdate', (event: any) => {
try {
const update = JSON.parse(event.data);
setStall(prevStall => {
if (!prevStall) return prevStall;
return {
...prevStall,
products: prevStall.products.map(p =>
p.id === update.productId.toString() ? { ...p, stock: update.stock } : p
)
};
});
} catch (err) {
console.error('Error processing stock update:', err);
}
});
return () => {
eventSource.close();
};
}, [id]);
if (loading) {
return (
<div className="container stall-detail-page loading">
<div className="loading-spinner">Loading Stall Details...</div>
</div>
);
}
if (error || !stall) {
return (
<div className="container stall-detail-page error">
<header className="detail-header sticky">
<button className="back-button" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
</header>
<div className="error-content">
<h2>Stall Not Found</h2>
<p>{error || "We couldn't find the stall you're looking for."}</p>
<button className="primary-button" onClick={() => navigate('/')}>Back to Home</button>
</div>
</div>
);
}
return (
<div className="container stall-detail-page">
<header className="detail-header">
<button className="back-button" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<div className="header-image-container">
{stall.imageData ? (
<img src={stall.imageData} alt={stall.name} className="header-bg-image" />
) : (
<div className="header-placeholder" />
)}
<div className="header-overlay"></div>
</div>
</header>
<main className="detail-content safe-area-bottom">
<div className="stall-main-card">
<div className="stall-header-info">
<h1 className="stall-title">{stall.name}</h1>
{stall.temporarilyClosed && (
<span className="status-tag closed">Closed Now</span>
)}
</div>
<p className="stall-description">{stall.description}</p>
<div className="stall-meta">
<div className="meta-item">
<Clock size={16} />
<span>Fast Preparation</span>
</div>
<div className="meta-item">
<Info size={16} />
<span>Clean & Hygienic</span>
</div>
</div>
</div>
<section className="stall-items-section">
<h2 className="section-title">Available Products</h2>
<div className="items-list">
{stall.products && stall.products.length > 0 ? (
stall.products.map((item, index) => (
<ItemCard
key={item.id}
item={item}
isLast={index === stall.products!.length - 1}
/>
))
) : (
<div className="no-items">
<p>No products available from this stall at the moment.</p>
</div>
)}
</div>
</section>
</main>
<CartTab />
<BottomNav />
</div>
);
};
export default StallDetailScreen;

View File

@@ -0,0 +1,90 @@
.success-page {
display: flex;
align-items: center;
justify-content: center;
}
.success-content {
padding: 40px 24px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.success-icon-wrapper {
color: var(--green);
margin-bottom: 32px;
animation: scaleIn 0.5s ease-out;
}
@keyframes scaleIn {
0% { transform: scale(0); }
70% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.success-title {
font-size: 26px;
font-weight: 800;
color: var(--text-dark);
margin-bottom: 8px;
}
.success-order-id {
font-size: 16px;
font-weight: 600;
color: var(--primary);
margin-bottom: 32px;
}
.qr-container {
background: white;
padding: 24px;
border-radius: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
margin-bottom: 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
border: 1px solid var(--border);
}
.qr-code {
max-width: 100%;
height: auto !important;
}
.qr-hint {
font-size: 12px;
color: var(--text-mid);
font-weight: 500;
}
.success-actions {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.success-actions button {
width: 100%;
padding: 14px;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
}
.primary-button {
background-color: var(--primary);
color: white;
box-shadow: 0 4px 15px rgba(35, 22, 81, 0.2);
}
.secondary-button {
background-color: var(--surface);
color: var(--text-mid);
border: 1px solid var(--border);
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { CheckCircle, Home, ShoppingBag } from 'lucide-react';
import { QRCodeCanvas } from 'qrcode.react';
import './SuccessScreen.css';
const SuccessScreen: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const state = location.state || {};
const orderNumber = state.orderNumber || 'ORD-TEST-UUID';
const displayOrderId = state.displayOrderId || '000';
const archived = state.archived || false;
const status = (state.status || 'PAID').toUpperCase();
return (
<div className="container success-page">
<div className="success-content">
<div className="success-icon-wrapper">
<CheckCircle size={80} className="success-icon" />
</div>
<h1 className="success-title">Order Placed!</h1>
<p className="success-order-id">Order ID #{displayOrderId}</p>
<div className={`qr-container ${status === 'COMPLETED' ? 'qr-completed' : ''} ${archived ? 'qr-expired' : ''}`}>
<div className="qr-wrapper-inner">
<QRCodeCanvas
value={orderNumber}
size={200}
level={"H"}
includeMargin={true}
className="qr-code"
/>
{status === 'COMPLETED' && (
<div className="qr-overlay">
<span className="overlay-text">COMPLETED</span>
</div>
)}
{archived && (
<div className="qr-overlay">
<span className="overlay-text expired">EXPIRED</span>
</div>
)}
</div>
<p className="qr-hint">
{archived
? 'This order has expired'
: status === 'COMPLETED'
? 'Order has been fulfilled'
: 'Scan this QR at the counter to collect your food'}
</p>
</div>
<div className="success-actions">
<button
className="secondary-button"
onClick={() => navigate('/orders')}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
>
<ShoppingBag size={18} /> My Orders
</button>
<button
className="primary-button"
onClick={() => navigate('/')}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
>
<Home size={18} /> Back to Home
</button>
</div>
</div>
</div>
);
};
export default SuccessScreen;

View File

@@ -0,0 +1,46 @@
export interface FoodItem {
id: string;
name: string;
description: string;
longDescription?: string;
price: number;
category: string;
image: string;
isVeg: boolean;
isPopular?: boolean;
stock?: number;
stallId?: string;
stallName?: string;
}
export interface Category {
id: string;
name: string;
emoji: string;
image: string;
color: string;
description?: string;
}
export interface CartItem extends FoodItem {
quantity: number;
stallId?: string;
stallName?: string;
}
export interface User {
id: number;
mobileNumber: string;
name: string;
isLoggedIn: boolean;
}
export interface Stall {
id: string;
name: string;
description: string;
imageData: string;
active: boolean;
temporarilyClosed: boolean;
products?: FoodItem[];
}