275 lines
7.6 KiB
TypeScript
275 lines
7.6 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
IonButton,
|
|
IonCard,
|
|
IonCardContent,
|
|
IonChip,
|
|
IonContent,
|
|
IonHeader,
|
|
IonIcon,
|
|
IonPage,
|
|
IonSearchbar,
|
|
IonText,
|
|
IonTitle,
|
|
IonToolbar,
|
|
useIonViewWillEnter,
|
|
} from '@ionic/react';
|
|
import {
|
|
bagHandleOutline,
|
|
cartOutline,
|
|
receiptOutline,
|
|
searchOutline,
|
|
star,
|
|
} from 'ionicons/icons';
|
|
import { useHistory } from 'react-router-dom';
|
|
import {
|
|
setStatusBarBackground,
|
|
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';
|
|
|
|
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: 'Ready to ship',
|
|
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: 'Baked today',
|
|
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: 'Picked fresh',
|
|
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: 'Stocked daily',
|
|
image: farmEggsImage,
|
|
imageAlt: 'Carton of free range farm eggs on a clean 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 Home: React.FC = () => {
|
|
const history = useHistory();
|
|
const [query, setQuery] = useState('');
|
|
const [cartCount, setCartCount] = useState(0);
|
|
|
|
useIonViewWillEnter(() => {
|
|
setStatusBarStyle(Style.Dark);
|
|
setStatusBarBackground('#fff8f2');
|
|
const count = readCart().reduce((sum, item) => sum + item.quantity, 0);
|
|
setCartCount(count);
|
|
});
|
|
|
|
useEffect(() => {
|
|
const syncCount = () => {
|
|
const count = readCart().reduce((sum, item) => sum + item.quantity, 0);
|
|
setCartCount(count);
|
|
};
|
|
|
|
syncCount();
|
|
window.addEventListener('storage', syncCount);
|
|
window.addEventListener('cart:updated', syncCount as EventListener);
|
|
return () => {
|
|
window.removeEventListener('storage', syncCount);
|
|
window.removeEventListener('cart:updated', syncCount as EventListener);
|
|
};
|
|
}, []);
|
|
|
|
const filteredProducts = useMemo(() => {
|
|
const term = query.toLowerCase().trim();
|
|
return products.filter((product) => {
|
|
if (!term) return true;
|
|
|
|
return [product.name, product.category, product.description]
|
|
.join(' ')
|
|
.toLowerCase()
|
|
.includes(term);
|
|
});
|
|
}, [query]);
|
|
|
|
return (
|
|
<IonPage>
|
|
<IonHeader className="shop-header ion-no-border">
|
|
<IonToolbar>
|
|
<IonTitle>QuickCart</IonTitle>
|
|
<IonButton
|
|
slot="end"
|
|
fill="clear"
|
|
className="header-icon-button"
|
|
onClick={() => history.push('/orders')}
|
|
aria-label="Track orders"
|
|
>
|
|
<IonIcon icon={receiptOutline} />
|
|
</IonButton>
|
|
<IonButton
|
|
slot="end"
|
|
fill="clear"
|
|
className="cart-button pill-cart-button"
|
|
onClick={() => history.push('/cart')}
|
|
aria-label="View cart"
|
|
>
|
|
<IonIcon icon={cartOutline} />
|
|
<span>{cartCount}</span>
|
|
</IonButton>
|
|
</IonToolbar>
|
|
</IonHeader>
|
|
|
|
<IonContent fullscreen className="shop-content">
|
|
<div className="hero-card">
|
|
<IonChip color="primary">Fast delivery</IonChip>
|
|
<h1>Find your next favorite meal.</h1>
|
|
<p>
|
|
Browse simple picks, add them to your cart, and place your order in
|
|
seconds.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="catalog-section ion-padding">
|
|
<IonSearchbar
|
|
value={query}
|
|
onIonInput={(event) => setQuery(event.detail.value ?? '')}
|
|
placeholder="Search products"
|
|
searchIcon={searchOutline}
|
|
/>
|
|
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2>Popular products</h2>
|
|
<IonText color="medium">
|
|
<p>{filteredProducts.length} items available</p>
|
|
</IonText>
|
|
</div>
|
|
</div>
|
|
|
|
{filteredProducts.length === 0 ? (
|
|
<div className="empty-state">
|
|
<IonIcon icon={bagHandleOutline} />
|
|
<h3>No products found</h3>
|
|
<p>Try a different search term to browse what is available.</p>
|
|
<IonButton onClick={() => setQuery('')}>Clear search</IonButton>
|
|
</div>
|
|
) : (
|
|
<div className="product-grid">
|
|
{filteredProducts.map((product) => (
|
|
<IonCard
|
|
key={product.id}
|
|
button
|
|
className="product-card"
|
|
onClick={() => history.push(`/product/${product.id}`)}
|
|
>
|
|
<img
|
|
src={product.image}
|
|
alt={product.imageAlt}
|
|
className="product-card-image"
|
|
/>
|
|
<IonCardContent>
|
|
<div className="product-card-top">
|
|
<IonChip color="secondary">{product.category}</IonChip>
|
|
<div className="rating-pill">
|
|
<IonIcon icon={star} />
|
|
<span>{product.rating}</span>
|
|
</div>
|
|
</div>
|
|
<h3>{product.name}</h3>
|
|
<p>{product.description}</p>
|
|
<div className="product-card-footer">
|
|
<strong>${product.price.toFixed(2)}</strong>
|
|
<span>{product.prepTime}</span>
|
|
</div>
|
|
</IonCardContent>
|
|
</IonCard>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default Home;
|