My Orders fixed
This commit is contained in:
5
ordering_site/src/App.css
Normal file
5
ordering_site/src/App.css
Normal file
@@ -0,0 +1,5 @@
|
||||
/* App level styles */
|
||||
#root {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
87
ordering_site/src/App.tsx
Normal file
87
ordering_site/src/App.tsx
Normal 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;
|
||||
BIN
ordering_site/src/assets/RIT_Logo.png
Normal file
BIN
ordering_site/src/assets/RIT_Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
BIN
ordering_site/src/assets/hero.png
Normal file
BIN
ordering_site/src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
1
ordering_site/src/assets/react.svg
Normal file
1
ordering_site/src/assets/react.svg
Normal 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 |
1
ordering_site/src/assets/vite.svg
Normal file
1
ordering_site/src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
44
ordering_site/src/components/BottomNav.css
Normal file
44
ordering_site/src/components/BottomNav.css
Normal 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;
|
||||
}
|
||||
44
ordering_site/src/components/BottomNav.tsx
Normal file
44
ordering_site/src/components/BottomNav.tsx
Normal 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;
|
||||
119
ordering_site/src/components/CartTab.css
Normal file
119
ordering_site/src/components/CartTab.css
Normal 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;
|
||||
}
|
||||
38
ordering_site/src/components/CartTab.tsx
Normal file
38
ordering_site/src/components/CartTab.tsx
Normal 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;
|
||||
175
ordering_site/src/components/Header.css
Normal file
175
ordering_site/src/components/Header.css
Normal 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;
|
||||
}
|
||||
}
|
||||
85
ordering_site/src/components/Header.tsx
Normal file
85
ordering_site/src/components/Header.tsx
Normal 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;
|
||||
216
ordering_site/src/components/ItemCard.css
Normal file
216
ordering_site/src/components/ItemCard.css
Normal 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;
|
||||
}
|
||||
68
ordering_site/src/components/ItemCard.tsx
Normal file
68
ordering_site/src/components/ItemCard.tsx
Normal 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;
|
||||
24
ordering_site/src/components/ProtectedRoute.tsx
Normal file
24
ordering_site/src/components/ProtectedRoute.tsx
Normal 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;
|
||||
121
ordering_site/src/contexts/AuthContext.tsx
Normal file
121
ordering_site/src/contexts/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
100
ordering_site/src/contexts/CartContext.tsx
Normal file
100
ordering_site/src/contexts/CartContext.tsx
Normal 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;
|
||||
};
|
||||
145
ordering_site/src/contexts/FoodContext.tsx
Normal file
145
ordering_site/src/contexts/FoodContext.tsx
Normal 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
142
ordering_site/src/index.css
Normal 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;
|
||||
}
|
||||
10
ordering_site/src/main.tsx
Normal file
10
ordering_site/src/main.tsx
Normal 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>,
|
||||
)
|
||||
233
ordering_site/src/pages/CartScreen.css
Normal file
233
ordering_site/src/pages/CartScreen.css
Normal 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;
|
||||
}
|
||||
}
|
||||
103
ordering_site/src/pages/CartScreen.tsx
Normal file
103
ordering_site/src/pages/CartScreen.tsx
Normal 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;
|
||||
46
ordering_site/src/pages/CategoryScreen.tsx
Normal file
46
ordering_site/src/pages/CategoryScreen.tsx
Normal 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;
|
||||
161
ordering_site/src/pages/ChangePinScreen.css
Normal file
161
ordering_site/src/pages/ChangePinScreen.css
Normal 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;
|
||||
}
|
||||
166
ordering_site/src/pages/ChangePinScreen.tsx
Normal file
166
ordering_site/src/pages/ChangePinScreen.tsx
Normal 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;
|
||||
186
ordering_site/src/pages/CheckoutScreen.css
Normal file
186
ordering_site/src/pages/CheckoutScreen.css
Normal 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;
|
||||
}
|
||||
}
|
||||
148
ordering_site/src/pages/CheckoutScreen.tsx
Normal file
148
ordering_site/src/pages/CheckoutScreen.tsx
Normal 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;
|
||||
275
ordering_site/src/pages/HomeScreen.css
Normal file
275
ordering_site/src/pages/HomeScreen.css
Normal 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);
|
||||
}
|
||||
186
ordering_site/src/pages/HomeScreen.tsx
Normal file
186
ordering_site/src/pages/HomeScreen.tsx
Normal 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;
|
||||
261
ordering_site/src/pages/ItemDetailScreen.css
Normal file
261
ordering_site/src/pages/ItemDetailScreen.css
Normal 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;
|
||||
}
|
||||
}
|
||||
112
ordering_site/src/pages/ItemDetailScreen.tsx
Normal file
112
ordering_site/src/pages/ItemDetailScreen.tsx
Normal 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;
|
||||
248
ordering_site/src/pages/LoginScreen.css
Normal file
248
ordering_site/src/pages/LoginScreen.css
Normal 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;
|
||||
}
|
||||
}
|
||||
222
ordering_site/src/pages/LoginScreen.tsx
Normal file
222
ordering_site/src/pages/LoginScreen.tsx
Normal 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;
|
||||
321
ordering_site/src/pages/MyOrdersScreen.css
Normal file
321
ordering_site/src/pages/MyOrdersScreen.css
Normal 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;
|
||||
}
|
||||
220
ordering_site/src/pages/MyOrdersScreen.tsx
Normal file
220
ordering_site/src/pages/MyOrdersScreen.tsx
Normal 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;
|
||||
207
ordering_site/src/pages/ProfileScreen.css
Normal file
207
ordering_site/src/pages/ProfileScreen.css
Normal 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);
|
||||
}
|
||||
106
ordering_site/src/pages/ProfileScreen.tsx
Normal file
106
ordering_site/src/pages/ProfileScreen.tsx
Normal 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;
|
||||
166
ordering_site/src/pages/StallDetailScreen.css
Normal file
166
ordering_site/src/pages/StallDetailScreen.css
Normal 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);
|
||||
}
|
||||
178
ordering_site/src/pages/StallDetailScreen.tsx
Normal file
178
ordering_site/src/pages/StallDetailScreen.tsx
Normal 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;
|
||||
90
ordering_site/src/pages/SuccessScreen.css
Normal file
90
ordering_site/src/pages/SuccessScreen.css
Normal 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);
|
||||
}
|
||||
76
ordering_site/src/pages/SuccessScreen.tsx
Normal file
76
ordering_site/src/pages/SuccessScreen.tsx
Normal 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;
|
||||
46
ordering_site/src/types/index.ts
Normal file
46
ordering_site/src/types/index.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user