843 lines
26 KiB
TypeScript
843 lines
26 KiB
TypeScript
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;
|