-
{item.name}
-
-
-
{rating}
+
+
+
{item.name}
+ {ratingCount > 0 && (
+
+
+ {rating.toFixed(1)} ({ratingCount})
+
+ )}
-
{item.stallName || 'Cookie Heaven'}
-
π 54 Summit Street
+
{item.stallName || 'Unknown Stall'}
e.stopPropagation()}>
π
‘{item.price.toFixed(2)}
diff --git a/ordering_site/src/contexts/CartContext.tsx b/ordering_site/src/contexts/CartContext.tsx
index 94b7d000..f11f008b 100644
--- a/ordering_site/src/contexts/CartContext.tsx
+++ b/ordering_site/src/contexts/CartContext.tsx
@@ -3,9 +3,10 @@ import type { FoodItem, CartItem } from '../types';
interface CartContextType {
cart: CartItem[];
- addToCart: (item: FoodItem) => void;
+ addToCart: (item: FoodItem, isParcel?: boolean) => void;
removeFromCart: (itemId: string) => void;
updateQuantity: (itemId: string, delta: number) => void;
+ toggleParcel: (itemId: string) => void;
clearCart: () => void;
getItemQuantity: (itemId: string) => number;
totalItems: number;
@@ -29,7 +30,7 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
const clearStockError = () => setStockError(null);
- const addToCart = (item: FoodItem) => {
+ const addToCart = (item: FoodItem, isParcel?: boolean) => {
setCart((prevCart) => {
const existingIndex = prevCart.findIndex((i) => i.id === item.id);
@@ -45,13 +46,22 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
newCart[existingIndex] = {
...newCart[existingIndex],
quantity: newCart[existingIndex].quantity + 1,
+ isParcel: isParcel !== undefined ? isParcel : newCart[existingIndex].isParcel
};
return newCart;
}
- return [...prevCart, { ...item, quantity: 1 }];
+ return [...prevCart, { ...item, quantity: 1, isParcel: !!isParcel }];
});
};
+ const toggleParcel = (itemId: string) => {
+ setCart((prevCart) =>
+ prevCart.map((item) =>
+ item.id === itemId ? { ...item, isParcel: !item.isParcel } : item
+ )
+ );
+ };
+
const removeFromCart = (itemId: string) => {
setCart((prevCart) => prevCart.filter((item) => item.id !== itemId));
};
@@ -92,7 +102,7 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
};
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
- const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
+ const totalPrice = cart.reduce((sum, item) => sum + (item.price + (item.isParcel ? 5 : 0)) * item.quantity, 0);
return (
= ({ children
addToCart,
removeFromCart,
updateQuantity,
+ toggleParcel,
clearCart,
getItemQuantity,
totalItems,
diff --git a/ordering_site/src/contexts/FoodContext.tsx b/ordering_site/src/contexts/FoodContext.tsx
index d7b581d2..660cc9ba 100644
--- a/ordering_site/src/contexts/FoodContext.tsx
+++ b/ordering_site/src/contexts/FoodContext.tsx
@@ -28,11 +28,20 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (!silent) setIsLoading(true);
setError(null);
try {
+ const token = localStorage.getItem('token');
+ const headers: Record = {};
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
const [baseItemsRes, productsRes, stallsRes, statsRes] = await Promise.all([
fetch(`${API_BASE_URL}/base-items?size=100`, { cache: 'no-store' }),
fetch(`${API_BASE_URL}/products?size=100`, { cache: 'no-store' }),
fetch(`${API_BASE_URL}/stalls/active`, { cache: 'no-store' }),
- fetch(`${API_BASE_URL}/feedback/stats`, { cache: 'no-store' }).catch(() => null)
+ fetch(`${API_BASE_URL}/feedback/stats`, {
+ cache: 'no-store',
+ headers
+ }).catch(() => null)
]);
if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) {
@@ -46,8 +55,9 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
const baseItemsData = baseItemsDataRaw.content || baseItemsDataRaw;
const productsData = productsDataRaw.content || productsDataRaw;
- // Extract top rated items names by customer feedback count
+ // Extract top rated items names by customer feedback count and rating maps
let topRatedNames: string[] = [];
+ const itemRatingsMap: Record = {};
if (statsRes && statsRes.ok) {
try {
const statsData = await statsRes.json();
@@ -59,6 +69,15 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
.filter((x: any) => x.count > 0)
.slice(0, 3)
.map((x: any) => x.name.toLowerCase());
+
+ ratedItems.forEach((x: any) => {
+ if (x.name) {
+ itemRatingsMap[x.name.toLowerCase()] = {
+ average: x.average,
+ count: x.count
+ };
+ }
+ });
} catch (e) {
console.error('Error parsing feedback stats:', e);
}
@@ -121,6 +140,7 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
// Dynamic bestseller isPopular flag based on feedback stats
const isBestseller = topRatedNames.includes(item.name.toLowerCase());
+ const ratingInfo = itemRatingsMap[item.name.toLowerCase()];
return {
id: itemId,
@@ -133,7 +153,10 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
isPopular: isBestseller,
stock: item.stock,
stallId: (stallFromBackend?.id || stallFromMap?.id || stallFromCategory?.id),
- stallName: (stallFromBackend?.name || stallFromMap?.name || stallFromCategory?.name)
+ stallName: (stallFromBackend?.name || stallFromMap?.name || stallFromCategory?.name),
+ rating: ratingInfo ? ratingInfo.average : 5.0,
+ ratingCount: ratingInfo ? ratingInfo.count : 0,
+ parcellable: item.parcellable
};
});
diff --git a/ordering_site/src/index.css b/ordering_site/src/index.css
index 74d159ea..577f11c7 100644
--- a/ordering_site/src/index.css
+++ b/ordering_site/src/index.css
@@ -232,3 +232,38 @@ main::-webkit-scrollbar {
.latest-qr-wrapper.qr-expired canvas {
filter: blur(6px) grayscale(100%) !important;
}
+
+/* Veg / Non-Veg Indicator Styling */
+.veg-nonveg-indicator {
+ width: 14px;
+ height: 14px;
+ border: 1.5px solid;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background-color: transparent;
+}
+
+.veg-nonveg-indicator.veg {
+ border-color: #10b981; /* emerald-500 */
+}
+
+.veg-nonveg-indicator.non-veg {
+ border-color: #ef4444; /* red-500 */
+}
+
+.veg-nonveg-indicator .dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+}
+
+.veg-nonveg-indicator.veg .dot {
+ background-color: #10b981;
+}
+
+.veg-nonveg-indicator.non-veg .dot {
+ background-color: #ef4444;
+}
diff --git a/ordering_site/src/pages/CartScreen.tsx b/ordering_site/src/pages/CartScreen.tsx
index be663fea..0e36194d 100644
--- a/ordering_site/src/pages/CartScreen.tsx
+++ b/ordering_site/src/pages/CartScreen.tsx
@@ -7,7 +7,7 @@ import './CartScreen.css';
const CartScreen: React.FC = () => {
const navigate = useNavigate();
- const { cart, updateQuantity, removeFromCart, totalPrice, totalItems } = useCart();
+ const { cart, updateQuantity, removeFromCart, toggleParcel, totalPrice, totalItems } = useCart();
if (cart.length === 0) {
return (
@@ -45,8 +45,23 @@ const CartScreen: React.FC = () => {
+ {item.parcellable && (
+
+
+
+ )}
+
-
π
‘{(item.price * item.quantity).toFixed(2)}
+
π
‘{((item.price + (item.isParcel ? 5 : 0)) * item.quantity).toFixed(2)}