chore: initial import
This commit is contained in:
95
frontend/pages/products/[slug].js
Normal file
95
frontend/pages/products/[slug].js
Normal file
@@ -0,0 +1,95 @@
|
||||
import Head from 'next/head';
|
||||
|
||||
import SiteLayout from '../../components/SiteLayout';
|
||||
import { buildMediaUrl, fetchProduct, fetchProducts } from '../../lib/api';
|
||||
import ProductCard from '../../components/ProductCard';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
|
||||
export default function ProductPage({ product, suggestions }) {
|
||||
const { addItem } = useCart();
|
||||
const imageUrl = buildMediaUrl(product.hero_image) || product.imageUrl;
|
||||
const price = Number(product.price || 0).toFixed(2);
|
||||
|
||||
return (
|
||||
<SiteLayout>
|
||||
<Head>
|
||||
<title>{product.name} · Full Moon Bakehouse</title>
|
||||
<meta name="description" content={product.short_description || product.description || ''} />
|
||||
</Head>
|
||||
|
||||
<div className="wrapper">
|
||||
<section className="product-detail">
|
||||
<div className="product-detail__gallery">
|
||||
<img src={imageUrl} alt={product.name} />
|
||||
</div>
|
||||
|
||||
<div className="product-detail__info">
|
||||
<button className="ghost-btn" onClick={() => history.back()} style={{ width: 'fit-content' }}>
|
||||
← Back to products
|
||||
</button>
|
||||
<h1>{product.name}</h1>
|
||||
<div className="product-detail__price">${price}</div>
|
||||
{product.short_description ? (
|
||||
<p className="product-detail__description">{product.short_description}</p>
|
||||
) : null}
|
||||
{product.description ? (
|
||||
<p className="product-detail__description">{product.description}</p>
|
||||
) : null}
|
||||
<div className="product-detail__meta">
|
||||
<span>Inventory: {product.inventory_quantity ?? '—'} available</span>
|
||||
<span>Max {product.max_per_order ?? '—'} per order</span>
|
||||
{product.allergens ? <span>Allergens: {product.allergens}</span> : null}
|
||||
{product.ingredients ? <span>Ingredients: {product.ingredients}</span> : null}
|
||||
</div>
|
||||
<div className="product-card__actions">
|
||||
<button className="btn btn-primary" type="button" onClick={() => addItem(product)}>
|
||||
Add to cart
|
||||
</button>
|
||||
{Number.isFinite(product.max_per_order) ? (
|
||||
<span className="cart-note">Max {product.max_per_order} per order</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{suggestions.length ? (
|
||||
<section className="section" style={{ paddingTop: 0 }}>
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>More from this week's bake</h2>
|
||||
<p>Round out your order with another loaf, bagel pack, or sweet.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="product-grid">
|
||||
{suggestions.map((item) => (
|
||||
<ProductCard key={item.id || item.slug} product={item} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</SiteLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ params }) {
|
||||
const { slug } = params;
|
||||
try {
|
||||
const product = await fetchProduct(slug);
|
||||
const products = await fetchProducts();
|
||||
const suggestions = products
|
||||
.filter((item) => item.slug !== slug)
|
||||
.slice(0, 4)
|
||||
.map((item) => ({ ...item, imageUrl: buildMediaUrl(item.hero_image), isSample: false }));
|
||||
|
||||
return {
|
||||
props: {
|
||||
product: { ...product, imageUrl: buildMediaUrl(product.hero_image), isSample: false },
|
||||
suggestions,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Product detail fallback', error);
|
||||
return { notFound: true };
|
||||
}
|
||||
}
|
||||
94
frontend/pages/products/index.js
Normal file
94
frontend/pages/products/index.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import Head from 'next/head';
|
||||
|
||||
import ProductCard from '../../components/ProductCard';
|
||||
import SiteLayout from '../../components/SiteLayout';
|
||||
import { buildMediaUrl, fetchCategories, fetchProducts } from '../../lib/api';
|
||||
|
||||
const SAMPLE_PRODUCTS = [];
|
||||
|
||||
function normalizeProducts(products = []) {
|
||||
return products.map((product) => ({
|
||||
...product,
|
||||
imageUrl: buildMediaUrl(product.hero_image),
|
||||
isSample: false,
|
||||
}));
|
||||
}
|
||||
|
||||
export default function ProductsPage({ products, categories }) {
|
||||
const items = normalizeProducts(products);
|
||||
|
||||
const groups = categories.reduce((acc, category) => {
|
||||
acc[category.id] = { category, items: [] };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const uncategorized = [];
|
||||
items.forEach((product) => {
|
||||
const categoryId = product.category?.id;
|
||||
if (categoryId && groups[categoryId]) {
|
||||
groups[categoryId].items.push(product);
|
||||
} else {
|
||||
uncategorized.push(product);
|
||||
}
|
||||
});
|
||||
|
||||
const orderedGroups = Object.values(groups).filter((group) => group.items.length);
|
||||
if (uncategorized.length) {
|
||||
orderedGroups.push({
|
||||
category: { name: 'Weekly specials' },
|
||||
items: uncategorized,
|
||||
});
|
||||
}
|
||||
|
||||
const hasProducts = orderedGroups.length > 0;
|
||||
|
||||
return (
|
||||
<SiteLayout>
|
||||
<Head>
|
||||
<title>Products · Full Moon Bakehouse</title>
|
||||
<meta name="description" content="Browse the current Full Moon Bakehouse offerings." />
|
||||
</Head>
|
||||
|
||||
<div className="wrapper">
|
||||
<section className="section">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Weekly menu</h2>
|
||||
<p>
|
||||
Availability updates in real time. Add items to your cart and checkout when you're
|
||||
ready.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasProducts ? (
|
||||
orderedGroups.map((group) => (
|
||||
<div key={group.category.id || group.category.name} className="section" style={{ paddingTop: 0 }}>
|
||||
<h3 className="section-title" style={{ fontFamily: 'Fraunces, serif', fontSize: '1.6rem' }}>
|
||||
{group.category.name}
|
||||
</h3>
|
||||
<div className="product-grid">
|
||||
{group.items.map((product) => (
|
||||
<ProductCard key={product.id || product.slug} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flash info">Menu items will appear here once the API is populated.</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</SiteLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
try {
|
||||
const [products, categories] = await Promise.all([fetchProducts(), fetchCategories()]);
|
||||
return { props: { products, categories } };
|
||||
} catch (error) {
|
||||
console.warn('Products page data fallback', error);
|
||||
return { props: { products: SAMPLE_PRODUCTS, categories: [] } };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user