Storefront & headless
Product Card Component
Generated from docs/plan/core/product-card-component/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
A reusable React component that renders a product card with image, price, stock status, rating, and quick add-to-cart button. Supports variants, custom fields, responsive layouts, dark mode, and full accessibility. Drop-and-customize in any Next.js storefront.
The problem
Merchants rebuild product cards from scratch in every storefront; each team designs the same component (image, price, rating, add-to-cart button, loading states). Teams waste weeks on responsive layouts, image optimization, currency formatting, and accessibility—work that should be standardized and reusable.
What it does
- Renders full Product type (title, description, image, price, stock_available, rating)
- Displays price in shop currency with symbol (formatted via Intl.NumberFormat, 100% accurate)
- Shows stock status: in stock (green), low stock (yellow, <5), out of stock (red, disabled button)
- Renders average rating and review count; links to reviews if available
- Quick add-to-cart button; opens variant-selection modal if product has multiple variants
- Add-to-wishlist button (integrates with wishlist-save-for-later feature)
- Responsive image with next/image optimization (lazy loading, WebP, srcset)
- Slot-based customization: header slot, footer slot, actions slot (merchant adds custom content)
- Dark mode support via CSS variables (no hardcoded colors)
- Full accessibility (ARIA labels, role=article, keyboard navigation, screen reader support)
- Lazy loading via Intersection Observer (component doesn’t render until viewport-visible)
- Loading skeleton state (shows placeholder while data loads)
- Error state fallback (failed image, missing data handled gracefully)
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Full variant selector with color/size swatches — that’s detailed checkout UX; component has a modal button only, merchant owns swatch design
- Reviews embedded in the card — reviews are a separate review-card component; card stays thin; footer slot can accept reviews
- Built-in A/B testing or event tracking — analytics is merchant-specific; component emits onChange callbacks, merchant integrates their tracker
- Multi-image carousel or lightbox — hero image only; full carousel is detail-page responsibility; merchant adds carousel in detail view
- Lazy hydration or Progressive Enhancement — React component, client-side only; Next.js handles SSR details
- Real-time stock sync or polling — component loads once; stock freshness is the app’s responsibility; polling is merchant’s concern
Data model
none
API
- No new routes. Component consumes Product type from existing GET /api/products/:id endpoint
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Component docs in admin portal with usage examples, props reference, and slot customization guide. Storybook or MDX examples showing all states (loading, error, out-of-stock, variants). Nothing to configure; component is 100% customizable via props and CSS.
The seam — why this is core
Core owns the component implementation, responsive layout, dark mode support, accessibility features, and slot API. Paid pack owns nothing; storefront components are developer infrastructure, not credentials or country-specific obligations.
Core owns the interface + honest component library; storefront components are infrastructure, not a support commitment or credential.
Dependencies
- React 18+ (peer dependency; customer installs)
- Next.js Image component for optimization (assumes Next.js 12+)
- TypeScript SDK (for type definitions of Product type)
- CSS variables for theming (inline + CSS modules)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Component renders a product card with image, title, price, and clickable Add-to-Cart button; button fires onAddToCart callback with product_id
- If product stock is 0, card displays ‘Out of stock’ and disables the add-to-cart button (button.disabled = true)
- Price displays in shop currency symbol (e.g., €99.99 for EUR, $99.99 for USD) without manual currency config
- Add-to-wishlist button calls onAddToWishlist(productId) when clicked; integrates with wishlist API
- Responsive layout adapts: mobile (240px), tablet (300px), desktop (400px) without merchant breakpoint tweaks
- Dark mode: when CSS variable —theme-mode is ‘dark’, all colors invert using precomputed CSS custom properties
- Full keyboard navigation: Tab cycles through buttons, Enter/Space activates, no focus loss
- Custom fields (e.g., color, brand) from product.customFields render in configurable footer slot
- Component exports TypeScript interface Product so consumers get full type safety; no
anytypes
Risks
If API returns an invalid image URL, next/image throws; component must catch and show fallback image. If shop currency is unsupported by Intl.NumberFormat, price renders as raw integer (validate currency on mount). If merchant customizes slot content with oversized text/images, card breaks responsive layout (CSS constraints needed). If CSS variables are not defined, card renders in light mode only (test both themes). If merchant adds onClick to an image without aria-label, accessibility breaks (component can’t enforce merchant’s markup).
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included (Polaris); Magento: included |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.