build: d805a69a-e437-4a58-a083-30e34be74fcd

This commit is contained in:
AppCakes
2026-06-09 20:54:25 +00:00
commit 8ef288f145
135 changed files with 15122 additions and 0 deletions
+842
View File
@@ -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;