build: d805a69a-e437-4a58-a083-30e34be74fcd
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user