build: d805a69a-e437-4a58-a083-30e34be74fcd
This commit is contained in:
@@ -0,0 +1,842 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
IonButton,
|
||||
IonChip,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonSearchbar,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonToast,
|
||||
useIonViewWillEnter,
|
||||
} from '@ionic/react';
|
||||
import {
|
||||
bagHandleOutline,
|
||||
cartOutline,
|
||||
chevronForwardOutline,
|
||||
homeOutline,
|
||||
locationOutline,
|
||||
micOutline,
|
||||
personCircleOutline,
|
||||
receiptOutline,
|
||||
searchOutline,
|
||||
storefrontOutline,
|
||||
timeOutline,
|
||||
} from 'ionicons/icons';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ProductFilters, { SortOption } from '../components/ProductFilters';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { setStatusBarStyle, Style } from '../utils/statusBar';
|
||||
import avocadoOilImage from '../assets/organic-avocado-oil.png';
|
||||
import sourdoughBreadImage from '../assets/artisan-sourdough-bread.png';
|
||||
import strawberriesPackImage from '../assets/fresh-strawberries-pack.png';
|
||||
import farmEggsImage from '../assets/farm-eggs-carton.png';
|
||||
import bananasImage from '../assets/bananas-bunch.png';
|
||||
import wholeMilkImage from '../assets/whole-milk-carton.png';
|
||||
import brownRiceImage from '../assets/brown-rice-bag.png';
|
||||
import chickenBreastImage from '../assets/chicken-breast-pack.png';
|
||||
import orangeJuiceImage from '../assets/orange-juice-bottle.png';
|
||||
import rolledOatsImage from '../assets/rolled-oats-pack.png';
|
||||
import greekYogurtImage from '../assets/greek-yogurt-cup.png';
|
||||
import peanutButterImage from '../assets/peanut-butter-jar.png';
|
||||
import spaghettiImage from '../assets/pasta-spaghetti-pack.png';
|
||||
import tomatoSauceImage from '../assets/tomato-sauce-jar.png';
|
||||
import cheddarCheeseImage from '../assets/cheddar-cheese-block.png';
|
||||
import lettuceImage from '../assets/lettuce-head.png';
|
||||
import carrotsImage from '../assets/carrots-bag.png';
|
||||
import potatoesImage from '../assets/potatoes-bag.png';
|
||||
import onionsImage from '../assets/onions-net-bag.png';
|
||||
import salmonImage from '../assets/salmon-fillet-pack.png';
|
||||
import sparklingWaterImage from '../assets/sparkling-water-pack.png';
|
||||
import coffeeBeansImage from '../assets/coffee-beans-bag.png';
|
||||
import greenTeaImage from '../assets/green-tea-box.png';
|
||||
import oliveOilImage from '../assets/olive-oil-bottle.png';
|
||||
|
||||
export type Product = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
price: number;
|
||||
rating: number;
|
||||
prepTime: string;
|
||||
image: string;
|
||||
imageAlt: string;
|
||||
};
|
||||
|
||||
export type CartItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export const CART_STORAGE_KEY = 'simple-shop-cart';
|
||||
export const LAST_ORDER_STORAGE_KEY = 'simple-shop-last-order';
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Organic Avocado Oil',
|
||||
category: 'Pantry',
|
||||
description:
|
||||
'Cold-pressed avocado oil for cooking, roasting, and healthy everyday meals.',
|
||||
price: 8.99,
|
||||
rating: 4.8,
|
||||
prepTime: '60 MIN',
|
||||
image: avocadoOilImage,
|
||||
imageAlt: 'Bottle of organic avocado oil with fresh avocados',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Artisan Sourdough Bread',
|
||||
category: 'Bakery',
|
||||
description:
|
||||
'Freshly baked sourdough loaf with a crisp crust and soft, airy center.',
|
||||
price: 7.49,
|
||||
rating: 4.6,
|
||||
prepTime: '45 MIN',
|
||||
image: sourdoughBreadImage,
|
||||
imageAlt: 'Fresh artisan sourdough bread loaf on a wooden board',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Fresh Strawberries Pack',
|
||||
category: 'Fruit',
|
||||
description:
|
||||
'Sweet, bright strawberries packed fresh for snacks, desserts, and smoothies.',
|
||||
price: 9.25,
|
||||
rating: 4.9,
|
||||
prepTime: '35 MIN',
|
||||
image: strawberriesPackImage,
|
||||
imageAlt: 'Pack of fresh strawberries with berries beside it',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Free Range Farm Eggs',
|
||||
category: 'Dairy & Eggs',
|
||||
description:
|
||||
'Farm-fresh eggs with rich yolks, ideal for breakfast, baking, and meal prep.',
|
||||
price: 4.5,
|
||||
rating: 4.7,
|
||||
prepTime: '55 MIN',
|
||||
image: farmEggsImage,
|
||||
imageAlt: 'Carton of free range farm eggs on a clean background',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Bananas Bunch',
|
||||
category: 'Fruit',
|
||||
description:
|
||||
'Naturally sweet ripe bananas for breakfast bowls, snacks, and smoothies.',
|
||||
price: 3.2,
|
||||
rating: 4.7,
|
||||
prepTime: '40 MIN',
|
||||
image: bananasImage,
|
||||
imageAlt: 'Fresh bunch of ripe yellow bananas',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Whole Milk Carton',
|
||||
category: 'Dairy & Eggs',
|
||||
description:
|
||||
'Creamy whole milk for cereal, coffee, cooking, and family breakfasts.',
|
||||
price: 3.99,
|
||||
rating: 4.5,
|
||||
prepTime: '30 MIN',
|
||||
image: wholeMilkImage,
|
||||
imageAlt: 'Carton of whole milk standing upright',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'Brown Rice Bag',
|
||||
category: 'Pantry',
|
||||
description:
|
||||
'Nutty wholegrain brown rice for bowls, sides, and meal prep dinners.',
|
||||
price: 5.75,
|
||||
rating: 4.6,
|
||||
prepTime: '50 MIN',
|
||||
image: brownRiceImage,
|
||||
imageAlt: 'Sealed bag of brown rice on a white background',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'Chicken Breast Pack',
|
||||
category: 'Meat & Seafood',
|
||||
description:
|
||||
'Lean chicken breast fillets for grilling, roasting, and protein-packed meals.',
|
||||
price: 11.9,
|
||||
rating: 4.8,
|
||||
prepTime: '70 MIN',
|
||||
image: chickenBreastImage,
|
||||
imageAlt: 'Pack of fresh chicken breast fillets',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'Orange Juice Bottle',
|
||||
category: 'Beverages',
|
||||
description:
|
||||
'Bright citrus orange juice for a refreshing breakfast or midday boost.',
|
||||
price: 4.8,
|
||||
rating: 4.6,
|
||||
prepTime: '25 MIN',
|
||||
image: orangeJuiceImage,
|
||||
imageAlt: 'Bottle of orange juice on a clean background',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: 'Rolled Oats Pack',
|
||||
category: 'Breakfast',
|
||||
description:
|
||||
'Hearty rolled oats for porridge, overnight oats, and healthy baking.',
|
||||
price: 4.25,
|
||||
rating: 4.7,
|
||||
prepTime: '35 MIN',
|
||||
image: rolledOatsImage,
|
||||
imageAlt: 'Pack of rolled oats on a white background',
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
name: 'Greek Yogurt Cup',
|
||||
category: 'Dairy & Eggs',
|
||||
description:
|
||||
'Thick plain Greek yogurt with a rich texture for breakfast and snacks.',
|
||||
price: 2.65,
|
||||
rating: 4.5,
|
||||
prepTime: '20 MIN',
|
||||
image: greekYogurtImage,
|
||||
imageAlt: 'Single cup of plain Greek yogurt',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
name: 'Peanut Butter Jar',
|
||||
category: 'Pantry',
|
||||
description:
|
||||
'Smooth peanut butter for toast, sandwiches, smoothies, and baking.',
|
||||
price: 5.2,
|
||||
rating: 4.8,
|
||||
prepTime: '45 MIN',
|
||||
image: peanutButterImage,
|
||||
imageAlt: 'Jar of peanut butter on a white background',
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
name: 'Spaghetti Pasta Pack',
|
||||
category: 'Pantry',
|
||||
description:
|
||||
'Classic spaghetti pasta for quick weeknight dinners and family meals.',
|
||||
price: 3.45,
|
||||
rating: 4.6,
|
||||
prepTime: '50 MIN',
|
||||
image: spaghettiImage,
|
||||
imageAlt: 'Pack of spaghetti pasta on a clean background',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
name: 'Tomato Sauce Jar',
|
||||
category: 'Pantry',
|
||||
description:
|
||||
'Rich tomato sauce for pasta, pizza bases, and simmered home cooking.',
|
||||
price: 3.95,
|
||||
rating: 4.7,
|
||||
prepTime: '55 MIN',
|
||||
image: tomatoSauceImage,
|
||||
imageAlt: 'Glass jar of tomato sauce',
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
name: 'Cheddar Cheese Block',
|
||||
category: 'Dairy & Eggs',
|
||||
description:
|
||||
'Sharp cheddar cheese block for sandwiches, snacks, sauces, and grating.',
|
||||
price: 6.15,
|
||||
rating: 4.7,
|
||||
prepTime: '35 MIN',
|
||||
image: cheddarCheeseImage,
|
||||
imageAlt: 'Block of cheddar cheese on a white background',
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
name: 'Fresh Lettuce Head',
|
||||
category: 'Vegetables',
|
||||
description:
|
||||
'Crisp green lettuce for salads, wraps, burgers, and light lunches.',
|
||||
price: 2.8,
|
||||
rating: 4.4,
|
||||
prepTime: '30 MIN',
|
||||
image: lettuceImage,
|
||||
imageAlt: 'Fresh green head of lettuce',
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
name: 'Carrots Bag',
|
||||
category: 'Vegetables',
|
||||
description:
|
||||
'Sweet crunchy carrots for roasting, soups, stir-fries, and lunch boxes.',
|
||||
price: 3.1,
|
||||
rating: 4.5,
|
||||
prepTime: '45 MIN',
|
||||
image: carrotsImage,
|
||||
imageAlt: 'Bag of fresh carrots on a clean background',
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
name: 'Potatoes Bag',
|
||||
category: 'Vegetables',
|
||||
description:
|
||||
'Versatile potatoes for frying, mashing, baking, and hearty dinners.',
|
||||
price: 4.75,
|
||||
rating: 4.6,
|
||||
prepTime: '55 MIN',
|
||||
image: potatoesImage,
|
||||
imageAlt: 'Mesh bag of potatoes',
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
name: 'Onions Net Bag',
|
||||
category: 'Vegetables',
|
||||
description:
|
||||
'Flavorful onions for stews, sauces, sautés, and everyday cooking.',
|
||||
price: 3.35,
|
||||
rating: 4.4,
|
||||
prepTime: '50 MIN',
|
||||
image: onionsImage,
|
||||
imageAlt: 'Net bag of onions on a white background',
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
name: 'Salmon Fillet Pack',
|
||||
category: 'Meat & Seafood',
|
||||
description:
|
||||
'Fresh salmon fillet packed for quick oven dinners and healthy lunches.',
|
||||
price: 13.99,
|
||||
rating: 4.8,
|
||||
prepTime: '70 MIN',
|
||||
image: salmonImage,
|
||||
imageAlt: 'Packaged salmon fillet on a clean background',
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
name: 'Sparkling Water Pack',
|
||||
category: 'Beverages',
|
||||
description:
|
||||
'Refreshing sparkling water multipack for hydration and light entertaining.',
|
||||
price: 6.8,
|
||||
rating: 4.5,
|
||||
prepTime: '25 MIN',
|
||||
image: sparklingWaterImage,
|
||||
imageAlt: 'Multipack of sparkling water cans',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
name: 'Coffee Beans Bag',
|
||||
category: 'Beverages',
|
||||
description:
|
||||
'Roasted coffee beans with a rich aroma for espresso and filter brews.',
|
||||
price: 9.6,
|
||||
rating: 4.8,
|
||||
prepTime: '40 MIN',
|
||||
image: coffeeBeansImage,
|
||||
imageAlt: 'Bag of roasted coffee beans on a white background',
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
name: 'Green Tea Box',
|
||||
category: 'Beverages',
|
||||
description:
|
||||
'Soothing green tea for a light daily ritual and calming warm drink.',
|
||||
price: 4.4,
|
||||
rating: 4.5,
|
||||
prepTime: '30 MIN',
|
||||
image: greenTeaImage,
|
||||
imageAlt: 'Box of green tea on a clean background',
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
name: 'Extra Virgin Olive Oil',
|
||||
category: 'Pantry',
|
||||
description:
|
||||
'Fragrant olive oil for salads, drizzling, roasting, and Mediterranean meals.',
|
||||
price: 10.5,
|
||||
rating: 4.9,
|
||||
prepTime: '60 MIN',
|
||||
image: oliveOilImage,
|
||||
imageAlt: 'Bottle of extra virgin olive oil on a white background',
|
||||
},
|
||||
];
|
||||
|
||||
export const readCart = (): CartItem[] => {
|
||||
if (typeof window === 'undefined') return [];
|
||||
try {
|
||||
const savedCart = window.localStorage.getItem(CART_STORAGE_KEY);
|
||||
if (!savedCart) return [];
|
||||
const parsed = JSON.parse(savedCart) as CartItem[];
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const writeCart = (cart: CartItem[]) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart));
|
||||
window.dispatchEvent(new CustomEvent('cart:updated'));
|
||||
};
|
||||
|
||||
const featuredSectionIds = ['12', '11', '23', '1', '24'];
|
||||
const homeLifestyleSectionIds = ['6', '9', '21', '22', '10'];
|
||||
const STORE_OPEN_HOUR = 8;
|
||||
const STORE_CLOSE_HOUR = 20;
|
||||
|
||||
const formatPlacedTime = (date: Date) =>
|
||||
date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const formatHourSlot = (hour24: number) => {
|
||||
const normalized = ((hour24 % 24) + 24) % 24;
|
||||
const hour12 = normalized % 12 === 0 ? 12 : normalized % 12;
|
||||
const suffix = normalized >= 12 ? 'PM' : 'AM';
|
||||
return `${hour12} ${suffix}`;
|
||||
};
|
||||
|
||||
const formatDeliveryWindow = (startHour24: number, endHour24: number) =>
|
||||
`${formatHourSlot(startHour24)}-${formatHourSlot(endHour24)}`;
|
||||
|
||||
const getDeliveryTiming = (now: Date) => {
|
||||
const currentHour = now.getHours();
|
||||
|
||||
let slotStart = STORE_OPEN_HOUR;
|
||||
let slotEnd = STORE_OPEN_HOUR + 1;
|
||||
|
||||
if (currentHour < STORE_OPEN_HOUR) {
|
||||
slotStart = STORE_OPEN_HOUR;
|
||||
slotEnd = STORE_OPEN_HOUR + 1;
|
||||
} else if (currentHour >= STORE_CLOSE_HOUR - 1) {
|
||||
slotStart = STORE_OPEN_HOUR;
|
||||
slotEnd = STORE_OPEN_HOUR + 1;
|
||||
} else {
|
||||
slotStart = currentHour + 1;
|
||||
slotEnd = Math.min(slotStart + 1, STORE_CLOSE_HOUR);
|
||||
}
|
||||
|
||||
const etaHour = slotStart;
|
||||
const etaMinute = now.getMinutes() < 30 ? 15 : 45;
|
||||
const etaDate = new Date(now);
|
||||
etaDate.setHours(etaHour, etaMinute, 0, 0);
|
||||
|
||||
if (currentHour >= STORE_CLOSE_HOUR - 1) {
|
||||
etaDate.setDate(etaDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
placedLabel: formatPlacedTime(now),
|
||||
windowLabel: formatDeliveryWindow(slotStart, slotEnd),
|
||||
etaLabel: etaDate.toLocaleTimeString([], {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const ProductShelf: React.FC<{
|
||||
title: string;
|
||||
products: Product[];
|
||||
onOpenProduct: (id: string) => void;
|
||||
onQuickAdd?: (product: Product) => void;
|
||||
}> = ({ title, products: shelfProducts, onOpenProduct, onQuickAdd }) => (
|
||||
<section className="shelf-section">
|
||||
<div className="shelf-section-header">
|
||||
<h2>{title}</h2>
|
||||
<IonButton fill="clear" className="view-all-button">
|
||||
View All
|
||||
</IonButton>
|
||||
</div>
|
||||
<div className="shelf-scroll">
|
||||
{shelfProducts.map((product) => (
|
||||
<button
|
||||
key={product.id}
|
||||
type="button"
|
||||
className="shelf-product-card"
|
||||
onClick={() => onOpenProduct(product.id)}
|
||||
>
|
||||
<div className="shelf-image-wrap">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.imageAlt}
|
||||
className="shelf-product-image"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="shelf-add-badge"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onQuickAdd?.(product);
|
||||
}}
|
||||
aria-label={`Add ${product.name} to cart`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="shelf-card-copy">
|
||||
<span className="browse-product-category">{product.category}</span>
|
||||
<h3 className="shelf-product-title">{product.name}</h3>
|
||||
<div className="shelf-price-block">
|
||||
<span className="shelf-price">${product.price.toFixed(2)}</span>
|
||||
</div>
|
||||
<p className="shelf-product-description">{product.description}</p>
|
||||
<div className="shelf-meta-pill">
|
||||
<IonIcon icon={timeOutline} />
|
||||
<span>{product.prepTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { user } = useAuth();
|
||||
const [query, setQuery] = useState('');
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
const [cartCount, setCartCount] = useState(0);
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [sortOption, setSortOption] = useState<SortOption>('popular');
|
||||
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
setStatusBarStyle(Style.Dark);
|
||||
const count = readCart().reduce((sum, item) => sum + item.quantity, 0);
|
||||
setCartCount(count);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const syncCount = () =>
|
||||
setCartCount(readCart().reduce((sum, item) => sum + item.quantity, 0));
|
||||
syncCount();
|
||||
window.addEventListener('storage', syncCount);
|
||||
window.addEventListener('cart:updated', syncCount as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('storage', syncCount);
|
||||
window.removeEventListener('cart:updated', syncCount as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setInterval(() => {
|
||||
setNow(new Date());
|
||||
}, 60000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const categories = useMemo(
|
||||
() => Array.from(new Set(products.map((product) => product.category))),
|
||||
[]
|
||||
);
|
||||
|
||||
const filteredProducts = useMemo(() => {
|
||||
const term = query.toLowerCase().trim();
|
||||
const nextProducts = products.filter((product) => {
|
||||
const matchesCategory =
|
||||
selectedCategory === 'All' || product.category === selectedCategory;
|
||||
const matchesQuery =
|
||||
!term ||
|
||||
[product.name, product.category, product.description]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(term);
|
||||
return matchesCategory && matchesQuery;
|
||||
});
|
||||
|
||||
const sorted = [...nextProducts];
|
||||
if (sortOption === 'price-asc') sorted.sort((a, b) => a.price - b.price);
|
||||
if (sortOption === 'price-desc') sorted.sort((a, b) => b.price - a.price);
|
||||
if (sortOption === 'name-asc') {
|
||||
sorted.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
if (sortOption === 'popular') sorted.sort((a, b) => b.rating - a.rating);
|
||||
return sorted;
|
||||
}, [query, selectedCategory, sortOption]);
|
||||
|
||||
const featuredProducts = useMemo(
|
||||
() => products.filter((product) => featuredSectionIds.includes(product.id)),
|
||||
[]
|
||||
);
|
||||
const lifestyleProducts = useMemo(
|
||||
() =>
|
||||
products.filter((product) =>
|
||||
homeLifestyleSectionIds.includes(product.id)
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const addressLabel = '22 Acacia St';
|
||||
const { placedLabel, windowLabel, etaLabel } = getDeliveryTiming(now);
|
||||
|
||||
const handleQuickAdd = (product: Product) => {
|
||||
const cart = readCart();
|
||||
const existingItem = cart.find((item) => item.id === product.id);
|
||||
|
||||
const nextCart = existingItem
|
||||
? cart.map((item) =>
|
||||
item.id === product.id
|
||||
? { ...item, quantity: item.quantity + 1 }
|
||||
: item
|
||||
)
|
||||
: [
|
||||
...cart,
|
||||
{
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
quantity: 1,
|
||||
},
|
||||
];
|
||||
|
||||
writeCart(nextCart);
|
||||
setCartCount(nextCart.reduce((sum, item) => sum + item.quantity, 0));
|
||||
setToastMessage(`${product.name} added to basket`);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader className="home-premium-header ion-no-border">
|
||||
<IonToolbar>
|
||||
<div className="home-premium-toolbar">
|
||||
<div className="home-premium-location-block">
|
||||
<span>Deliver to</span>
|
||||
<button type="button" className="home-premium-location-button">
|
||||
<IonIcon icon={locationOutline} />
|
||||
<strong>{addressLabel}</strong>
|
||||
<IonIcon icon={chevronForwardOutline} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="home-premium-header-actions">
|
||||
<div className="home-open-pill">Open</div>
|
||||
<button
|
||||
type="button"
|
||||
className="home-premium-cart-button"
|
||||
onClick={() => history.push('/cart')}
|
||||
aria-label="Open basket"
|
||||
>
|
||||
<IonIcon icon={cartOutline} />
|
||||
<span>{cartCount}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent
|
||||
fullscreen
|
||||
className="hybrid-home-content premium-home-content"
|
||||
>
|
||||
<div className="home-premium-hero">
|
||||
<div className="home-premium-hero-copy">
|
||||
<span>Fresh groceries, fast</span>
|
||||
<h1>Shop today's picks</h1>
|
||||
<p>Curated essentials delivered in your next available slot.</p>
|
||||
</div>
|
||||
|
||||
<div className="home-premium-chip-grid">
|
||||
<div className="home-premium-chip">
|
||||
<IonIcon icon={timeOutline} />
|
||||
<div>
|
||||
<span>Placed</span>
|
||||
<strong>{placedLabel}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="home-premium-chip">
|
||||
<IonIcon icon={bagHandleOutline} />
|
||||
<div>
|
||||
<span>Delivery</span>
|
||||
<strong>{windowLabel}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="home-premium-search-card">
|
||||
<IonSearchbar
|
||||
value={query}
|
||||
onIonInput={(event) => setQuery(event.detail.value ?? '')}
|
||||
placeholder="Search products and brands"
|
||||
searchIcon={searchOutline}
|
||||
/>
|
||||
<div className="home-premium-search-actions">
|
||||
<IonIcon icon={storefrontOutline} />
|
||||
<IonIcon icon={micOutline} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="home-premium-tracking-card"
|
||||
onClick={() => history.push('/orders')}
|
||||
>
|
||||
<div className="home-premium-tracking-icon">
|
||||
<IonIcon icon={bagHandleOutline} />
|
||||
</div>
|
||||
<div className="home-premium-tracking-copy">
|
||||
<span>Order tracking</span>
|
||||
<strong>Order placed</strong>
|
||||
<p>ETA {etaLabel}</p>
|
||||
</div>
|
||||
<IonIcon icon={chevronForwardOutline} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="home-shelves-area hybrid-shelves-area premium-shelves-area">
|
||||
<ProductShelf
|
||||
title="GROCERIES PAYDAY DEALS"
|
||||
products={featuredProducts}
|
||||
onOpenProduct={(id) => history.push(`/product/${id}`)}
|
||||
onQuickAdd={handleQuickAdd}
|
||||
/>
|
||||
|
||||
<ProductShelf
|
||||
title="HOME & LIFESTYLE PAYDAY DEALS"
|
||||
products={lifestyleProducts}
|
||||
onOpenProduct={(id) => history.push(`/product/${id}`)}
|
||||
onQuickAdd={handleQuickAdd}
|
||||
/>
|
||||
|
||||
<div className="browse-section">
|
||||
<div className="section-heading reference-section-heading">
|
||||
<div>
|
||||
<h2>Browse everything</h2>
|
||||
<IonText color="medium">
|
||||
<p>{filteredProducts.length} products available</p>
|
||||
</IonText>
|
||||
</div>
|
||||
</div>
|
||||
<ProductFilters
|
||||
categories={categories}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
sortOption={sortOption}
|
||||
setSortOption={setSortOption}
|
||||
isOpen={isFilterOpen}
|
||||
onClose={() => setIsFilterOpen((value) => !value)}
|
||||
/>
|
||||
{filteredProducts.length === 0 ? (
|
||||
<div className="empty-state reference-empty-state">
|
||||
<IonIcon icon={bagHandleOutline} />
|
||||
<h3>No products found</h3>
|
||||
<p>Try a different search term or category.</p>
|
||||
<IonButton
|
||||
onClick={() => {
|
||||
setQuery('');
|
||||
setSelectedCategory('All');
|
||||
}}
|
||||
>
|
||||
Clear filters
|
||||
</IonButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="browse-product-grid">
|
||||
{filteredProducts.map((product) => (
|
||||
<button
|
||||
key={product.id}
|
||||
type="button"
|
||||
className="browse-product-card"
|
||||
onClick={() => history.push(`/product/${product.id}`)}
|
||||
>
|
||||
<div className="browse-product-image-wrap">
|
||||
<img src={product.image} alt={product.imageAlt} />
|
||||
<button
|
||||
type="button"
|
||||
className="browse-add-badge"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleQuickAdd(product);
|
||||
}}
|
||||
aria-label={`Add ${product.name} to cart`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="browse-product-copy">
|
||||
<span className="browse-product-category">
|
||||
{product.category}
|
||||
</span>
|
||||
<h3>{product.name}</h3>
|
||||
<div className="browse-price-row">
|
||||
<strong>${product.price.toFixed(2)}</strong>
|
||||
<div className="browse-meta-pill">
|
||||
<IonIcon icon={timeOutline} />
|
||||
<span>{product.prepTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>{product.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bottom-nav-spacer" />
|
||||
<IonToast
|
||||
isOpen={Boolean(toastMessage)}
|
||||
message={toastMessage}
|
||||
duration={1800}
|
||||
position="bottom"
|
||||
onDidDismiss={() => setToastMessage('')}
|
||||
/>
|
||||
|
||||
<div className="reference-bottom-nav">
|
||||
<button type="button" className="bottom-nav-item active">
|
||||
<IonIcon icon={homeOutline} />
|
||||
<span>Home</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bottom-nav-item"
|
||||
onClick={() => setIsFilterOpen(true)}
|
||||
>
|
||||
<IonIcon icon={searchOutline} />
|
||||
<span>Discover</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bottom-nav-item"
|
||||
onClick={() => history.push('/orders')}
|
||||
>
|
||||
<IonIcon icon={storefrontOutline} />
|
||||
<span>My Shop</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bottom-nav-item"
|
||||
onClick={() => history.push(user ? '/profile' : '/auth')}
|
||||
>
|
||||
<IonIcon icon={personCircleOutline} />
|
||||
<span>My Profile</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bottom-nav-item"
|
||||
onClick={() => history.push('/cart')}
|
||||
>
|
||||
<IonIcon icon={cartOutline} />
|
||||
<span>Basket</span>
|
||||
</button>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
Reference in New Issue
Block a user