Repeat orders added
This commit is contained in:
47
ordering_site/package-lock.json
generated
47
ordering_site/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "ordering_site",
|
"name": "ordering_site",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -1834,6 +1835,33 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.38.0",
|
||||||
|
"motion-utils": "^12.36.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -2398,6 +2426,21 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.36.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -2824,9 +2867,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"license": "0BSD"
|
||||||
"license": "0BSD",
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface AuthContextType {
|
|||||||
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
|
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
|
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
|
||||||
|
updateProfile: (name: string, mobileNumber: string) => Promise<{ success: boolean; message: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
@@ -29,7 +30,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
const checkUserExists = async (mobileNumber: string) => {
|
const checkUserExists = async (mobileNumber: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/check`, {
|
const response = await fetch(`${API_BASE_URL}/check`, { cache: 'no-store',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ mobileNumber }),
|
body: JSON.stringify({ mobileNumber }),
|
||||||
@@ -42,7 +43,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
const login = async (mobileNumber: string, pin: string) => {
|
const login = async (mobileNumber: string, pin: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/login`, {
|
const response = await fetch(`${API_BASE_URL}/login`, { cache: 'no-store',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ mobileNumber, pin }),
|
body: JSON.stringify({ mobileNumber, pin }),
|
||||||
@@ -61,7 +62,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
const register = async (mobileNumber: string, name: string, pin: string) => {
|
const register = async (mobileNumber: string, name: string, pin: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/register`, {
|
const response = await fetch(`${API_BASE_URL}/register`, { cache: 'no-store',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ mobileNumber, name, pin }),
|
body: JSON.stringify({ mobileNumber, name, pin }),
|
||||||
@@ -80,7 +81,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
fetch(`${API_BASE_URL}/logout`, {
|
fetch(`${API_BASE_URL}/logout`, { cache: 'no-store',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ mobileNumber: user.mobileNumber }),
|
body: JSON.stringify({ mobileNumber: user.mobileNumber }),
|
||||||
@@ -93,7 +94,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const changePin = async (currentPin: string, newPin: string) => {
|
const changePin = async (currentPin: string, newPin: string) => {
|
||||||
if (!user) return { success: false, message: 'Not logged in' };
|
if (!user) return { success: false, message: 'Not logged in' };
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/change-pin`, {
|
const response = await fetch(`${API_BASE_URL}/change-pin`, { cache: 'no-store',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ mobileNumber: user.mobileNumber, currentPin, newPin }),
|
body: JSON.stringify({ mobileNumber: user.mobileNumber, currentPin, newPin }),
|
||||||
@@ -105,8 +106,38 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateProfile = async (name: string, mobileNumber: string) => {
|
||||||
|
if (!user) return { success: false, message: 'Not logged in' };
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/users/${user.id}`, { cache: 'no-store',
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, mobileNumber }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const updatedUserDto = await response.json();
|
||||||
|
// The DTO from backend might have different structure, let's map it back to User type
|
||||||
|
const updatedUser: User = {
|
||||||
|
id: updatedUserDto.id,
|
||||||
|
name: updatedUserDto.name,
|
||||||
|
mobileNumber: updatedUserDto.mobileNumber,
|
||||||
|
isLoggedIn: updatedUserDto.loggedIn
|
||||||
|
};
|
||||||
|
setUser(updatedUser);
|
||||||
|
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||||
|
return { success: true, message: 'Profile updated successfully' };
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
return { success: false, message: errorData.message || 'Failed to update profile' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: 'Network error. Please try again.' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin }}>
|
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin, updateProfile }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
|
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
|
||||||
fetch(`${API_BASE_URL}/base-items?size=100`),
|
fetch(`${API_BASE_URL}/base-items?size=100`, { cache: 'no-store' }),
|
||||||
fetch(`${API_BASE_URL}/products?size=100`),
|
fetch(`${API_BASE_URL}/products?size=100`, { cache: 'no-store' }),
|
||||||
fetch(`${API_BASE_URL}/stalls/active`)
|
fetch(`${API_BASE_URL}/stalls/active`, { cache: 'no-store' })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) {
|
if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) {
|
||||||
|
|||||||
@@ -319,3 +319,148 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Repeat Order Button */
|
||||||
|
.repeat-order-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 15px rgba(35, 22, 81, 0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeat-order-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeat-order-btn.mini {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
color: var(--primary);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stock Alert Modal */
|
||||||
|
.stock-alert-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 3000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-alert-modal {
|
||||||
|
background: white;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
border-radius: 28px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||||
|
animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalPop {
|
||||||
|
from { transform: scale(0.8); opacity: 0; }
|
||||||
|
to { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-icon-wrapper {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: #fff1f2;
|
||||||
|
color: #e11d48;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-alert-modal h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-dark);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-alert-modal p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-mid);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable-items-list {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable-item-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e11d48;
|
||||||
|
background: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #fecdd3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-proceed {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-discard {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-mid);
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X } from 'lucide-react';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle, ShoppingCart } from 'lucide-react';
|
||||||
import { QRCodeCanvas } from 'qrcode.react';
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
import BottomNav from '../components/BottomNav';
|
import BottomNav from '../components/BottomNav';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { useFood } from '../contexts/FoodContext';
|
||||||
|
import { useCart } from '../contexts/CartContext';
|
||||||
|
import type { FoodItem } from '../types';
|
||||||
import './MyOrdersScreen.css';
|
import './MyOrdersScreen.css';
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
@@ -23,11 +28,20 @@ interface Order {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MyOrdersScreen: React.FC = () => {
|
const MyOrdersScreen: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { foodItems } = useFood();
|
||||||
|
const { addToCart, clearCart } = useCart();
|
||||||
|
|
||||||
const [orders, setOrders] = useState<Order[]>([]);
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
||||||
|
|
||||||
|
// State for stock alert
|
||||||
|
const [showStockAlert, setShowStockAlert] = useState(false);
|
||||||
|
const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
|
||||||
|
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
@@ -46,6 +60,50 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRepeatOrder = (order: Order) => {
|
||||||
|
const missing: string[] = [];
|
||||||
|
const available: FoodItem[] = [];
|
||||||
|
|
||||||
|
order.items.forEach(orderItem => {
|
||||||
|
// Find the current food item by name (since orders store names)
|
||||||
|
// Robustness: matching by name is fallback if IDs aren't directly available in OrderItem DTO
|
||||||
|
const currentItem = foodItems.find(fi => fi.name === orderItem.productName);
|
||||||
|
|
||||||
|
if (currentItem) {
|
||||||
|
if (currentItem.stock && currentItem.stock > 0) {
|
||||||
|
// Add multiple times based on original quantity
|
||||||
|
for (let i = 0; i < orderItem.quantity; i++) {
|
||||||
|
available.push(currentItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing.push(orderItem.productName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing.push(orderItem.productName); // Item no longer exists in catalog
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
setUnavailableItems(missing);
|
||||||
|
setPendingItems(available);
|
||||||
|
setShowStockAlert(true);
|
||||||
|
} else {
|
||||||
|
// All items available
|
||||||
|
performRepeat(available);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const performRepeat = (items: FoodItem[]) => {
|
||||||
|
clearCart();
|
||||||
|
items.forEach(item => addToCart(item));
|
||||||
|
navigate('/cart');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProceedPartial = () => {
|
||||||
|
performRepeat(pendingItems);
|
||||||
|
setShowStockAlert(false);
|
||||||
|
};
|
||||||
|
|
||||||
const latestOrder = orders.length > 0 ? orders[0] : null;
|
const latestOrder = orders.length > 0 ? orders[0] : null;
|
||||||
const previousOrders = orders.length > 1 ? orders.slice(1) : [];
|
const previousOrders = orders.length > 1 ? orders.slice(1) : [];
|
||||||
|
|
||||||
@@ -108,6 +166,16 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
<span>Total Amount</span>
|
<span>Total Amount</span>
|
||||||
<span className="amount-text">₹{latestOrder.totalAmount.toFixed(2)}</span>
|
<span className="amount-text">₹{latestOrder.totalAmount.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="repeat-order-btn mini"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRepeatOrder(latestOrder);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCcw size={16} /> Repeat Order
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@@ -143,9 +211,23 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Order Detail Modal / Overlay */}
|
{/* Order Detail Modal / Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
{selectedOrder && (
|
{selectedOrder && (
|
||||||
<div className="order-detail-overlay" onClick={() => setSelectedOrder(null)}>
|
<motion.div
|
||||||
<div className="order-detail-modal" onClick={(e) => e.stopPropagation()}>
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="order-detail-overlay"
|
||||||
|
onClick={() => setSelectedOrder(null)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: '100%' }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: '100%' }}
|
||||||
|
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||||
|
className="order-detail-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<button className="close-details" onClick={() => setSelectedOrder(null)}>
|
<button className="close-details" onClick={() => setSelectedOrder(null)}>
|
||||||
<X size={24} />
|
<X size={24} />
|
||||||
</button>
|
</button>
|
||||||
@@ -208,9 +290,62 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
<span>₹{selectedOrder.totalAmount.toFixed(2)}</span>
|
<span>₹{selectedOrder.totalAmount.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button className="repeat-order-btn" onClick={() => handleRepeatOrder(selectedOrder)}>
|
||||||
|
<RefreshCcw size={18} /> Repeat this order
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Stock Alert Dialog */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showStockAlert && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="stock-alert-overlay"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.9, opacity: 0 }}
|
||||||
|
className="stock-alert-modal"
|
||||||
|
>
|
||||||
|
<div className="alert-icon-wrapper">
|
||||||
|
<AlertCircle size={32} />
|
||||||
</div>
|
</div>
|
||||||
|
<h3>Items Unavailable</h3>
|
||||||
|
<p>Some items from your previous order are currently out of stock or unavailable.</p>
|
||||||
|
|
||||||
|
<div className="unavailable-items-list">
|
||||||
|
{unavailableItems.map((item, idx) => (
|
||||||
|
<div key={idx} className="unavailable-item-chip">
|
||||||
|
<X size={14} /> {item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="alert-actions">
|
||||||
|
{pendingItems.length > 0 ? (
|
||||||
|
<button className="btn-proceed" onClick={handleProceedPartial}>
|
||||||
|
Proceed without these items
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginBottom: '12px', fontSize: '13px', color: '#e11d48', fontWeight: '600' }}>
|
||||||
|
All items in this order are currently unavailable.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<button className="btn-discard" onClick={() => setShowStockAlert(false)}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -205,3 +205,140 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
background: var(--surface);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
animation: modalIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalIn {
|
||||||
|
from { transform: translateY(20px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 24px 24px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: var(--bg);
|
||||||
|
border: none;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-mid);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 12px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-mid);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 16px 16px 48px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 0 0 4px var(--primary-light);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-mid);
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
flex: 2;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 4px 15px rgba(35, 22, 81, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
@@ -7,16 +7,33 @@ import {
|
|||||||
LogOut,
|
LogOut,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Phone,
|
Phone,
|
||||||
ShieldCheck
|
ShieldCheck,
|
||||||
|
User,
|
||||||
|
X,
|
||||||
|
User as UserIcon
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
import BottomNav from '../components/BottomNav';
|
import BottomNav from '../components/BottomNav';
|
||||||
import './ProfileScreen.css';
|
import './ProfileScreen.css';
|
||||||
|
|
||||||
const ProfileScreen: React.FC = () => {
|
const ProfileScreen: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout, updateProfile } = useAuth();
|
||||||
|
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [editName, setEditName] = useState(user?.name || '');
|
||||||
|
const [editMobile, setEditMobile] = useState(user?.mobileNumber || '');
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setEditName(user.name);
|
||||||
|
setEditMobile(user.mobileNumber);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (window.confirm('Are you sure you want to logout?')) {
|
if (window.confirm('Are you sure you want to logout?')) {
|
||||||
@@ -25,6 +42,39 @@ const ProfileScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setEditName(user?.name || '');
|
||||||
|
setEditMobile(user?.mobileNumber || '');
|
||||||
|
setError(null);
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateProfile = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!editName.trim() || !editMobile.trim()) {
|
||||||
|
setError('Name and mobile number are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[0-9]{10}$/.test(editMobile)) {
|
||||||
|
setError('Mobile number must be 10 digits');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUpdating(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const result = await updateProfile(editName, editMobile);
|
||||||
|
setIsUpdating(false);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
// Optional: Show success message
|
||||||
|
} else {
|
||||||
|
setError(result.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
||||||
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
||||||
@@ -49,10 +99,9 @@ const ProfileScreen: React.FC = () => {
|
|||||||
<h2 className="user-name">{user?.name || 'User Name'}</h2>
|
<h2 className="user-name">{user?.name || 'User Name'}</h2>
|
||||||
<div className="user-contact">
|
<div className="user-contact">
|
||||||
<span className="contact-item"><Phone size={14} /> {user?.mobileNumber || '+91 XXXXX XXXXX'}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<button className="edit-btn">Edit</button>
|
<button className="edit-btn" onClick={handleEditClick}>Edit</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -98,6 +147,70 @@ const ProfileScreen: React.FC = () => {
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Edit Profile Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isEditModalOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="modal-overlay"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||||
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
|
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||||
|
className="modal-container"
|
||||||
|
>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Edit Profile</h2>
|
||||||
|
<button type="button" className="close-btn" onClick={() => setIsEditModalOpen(false)}>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleUpdateProfile}>
|
||||||
|
<div className="modal-body">
|
||||||
|
{error && <div className="error-message" style={{ color: '#ff4d4f', fontSize: '0.85rem', marginBottom: '16px', fontWeight: '600' }}>{error}</div>}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Full Name</label>
|
||||||
|
<div className="input-wrapper">
|
||||||
|
<UserIcon className="input-icon" size={20} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
|
placeholder="Enter your name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Mobile Number</label>
|
||||||
|
<div className="input-wrapper">
|
||||||
|
<Phone className="input-icon" size={20} />
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={editMobile}
|
||||||
|
onChange={(e) => setEditMobile(e.target.value)}
|
||||||
|
placeholder="10-digit mobile number"
|
||||||
|
maxLength={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="btn-cancel" onClick={() => setIsEditModalOpen(false)}>Cancel</button>
|
||||||
|
<button type="submit" className="btn-save" disabled={isUpdating}>
|
||||||
|
{isUpdating ? 'Updating...' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user