Product Detail
Installation
Run the following command
npx vibes@latest add product-detail
Usage
import { ProductDetail } from '@/vibes/soul/sections/product-detail';function Usage() { return ( <ProductDetail action={action} breadcrumbs={breadcrumbs} fields={fieldsPromise} product={product} /> );}export const fields = [ { type: 'button-radio-group', label: 'Size', name: 'size', options: [ { value: 'sm', label: 'Small' }, { value: 'md', label: 'Medium' }, { value: 'lg', label: 'Large' }, ], required: true, }] export async function action( prevState: { fields: Field[]; lastResult: SubmissionResult | null }, payload: FormData,) { 'use server'; const submission = parseWithZod(payload, { schema: schema(fields) }); if (submission.status !== 'success') { return { fields: prevState.fields, lastResult: submission.reply(), }; } // Simulate add to cart await new Promise((resolve) => setTimeout(resolve, 1000)); return { fields: prevState.fields, lastResult: submission.reply({}), successMessage: 'Product(s) added to cart!', };}export const accordions = [ { title: 'What is your return policy?', content: 'We want you to be completely satisfied with your purchase. If you're not happy with your bike pack, you can return it within 30 days of delivery. Please ensure the pack is in its original condition and packaging. For detailed return instructions, visit our Return Policy page or contact our customer support team.', },];export const product = { id: '1', title: 'Mini Bar Bag', price: '$60', image: { src: 'https://storage.googleapis.com/s.mkswft.com/RmlsZTo1YzIwNTljMi04NzcwLTRiM2ItYmIzMy02ZTk0ODNkY2M5MDk=/mini-bar-bag.jpeg', alt: 'A close-up of a bicycle handlebar with a brown handlebar bag.', }, images: [ { src: 'https://storage.googleapis.com/s.mkswft.com/RmlsZTo1YzIwNTljMi04NzcwLTRiM2ItYmIzMy02ZTk0ODNkY2M5MDk=/mini-bar-bag.jpeg', alt: 'A close-up of a bicycle handlebar with a brown handlebar bag.', }, ], href: '#', rating: 4.8, summary: 'A sleek, versatile bike bag designed to fit various bikes while holding essentials like snacks, phone, and tools. Multiple mounts ensure a secure, streamlined fit.', description: 'Svelte and functional, this is one bag that goes well with every bike. We made this smaller so it fits little bikes and still carries the essentials - snacks, wallet, phone, keys, a tube, and tools. With multiple mounting positions, the fit can be dialed for short head-tubed mountain bikes, long stemmed road bikes, and everything in-between. The slim top edge is designed to fit behind mountain bike cables and tuck up neatly under computers, lights, and other accessories.', accordions,};
API Reference
ProductDetailProps<F>
Prop | Type | Default |
---|---|---|
breadcrumbs | ||
product | ||
action | ProductDetailFormAction<F> | |
fields | Streamable <F[]> | |
quantityLabel | string | |
incrementLabel | string | |
decrementLabel | string | |
ctaLabel | Streamable <string | null> | |
ctaDisabled | Streamable <boolean | null> | |
prefetch | boolean | |
thumbnailLabel | string | |
additionalInformationTitle | string |
ProductDetailProduct
Prop | Type | Default |
---|---|---|
id | string | |
title | string | |
href | string | |
images | ||
price | ||
subtitle | string | |
badge | string | |
rating | Streamable <number | null> | |
summary | Streamable <string> | |
description | Streamable <string | ReactNode | null> | |
accordions |
Acorrdion
Prop | Type | Default |
---|---|---|
title* | string | |
content* | ReactNode |
Image
Prop | Type | Default |
---|---|---|
src* | string | |
alt* | string |
PriceRange
Prop | Type | Default |
---|---|---|
type | 'range' | |
minValue | string | |
maxValue | string |
PriceSale
Prop | Type | Default |
---|---|---|
type | 'sale' | |
previousValue | string | |
currentValue | string |
ProductDetailFormAction
interface State<F extends Field> { fields: F[]; lastResult: SubmissionResult | null; successMessage?: ReactNode;}export type ProductDetailFormAction<F extends Field> = Action<State<F>, FormData>;
This component uses Confom to handle form submissions. Refer to the Conform docs for more details.
Here's an example of an action function that does validation and simulates sending an email:
export async function action( prevState: { fields: Field[]; lastResult: SubmissionResult | null }, payload: FormData,) { 'use server'; const submission = parseWithZod(payload, { schema: schema(fields) }); if (submission.status !== 'success') { return { fields: prevState.fields, lastResult: submission.reply(), }; } await new Promise((resolve) => setTimeout(resolve, 1000)); return { fields: prevState.fields, lastResult: submission.reply({}), successMessage: 'Product(s) added to cart!', };}
Breadcrumb
Prop | Type | Default |
---|---|---|
label* | string | |
href* | string |
CSS Variables
This component supports various CSS variables for theming. Here's a comprehensive list.
:root { --product-detail-border: var(--contrast-100); --product-detail-subtitle-font-family: var(--font-family-mono); --product-detail-title-font-family: var(--font-family-heading); --product-detail-primary-text: var(--foreground); --product-detail-secondary-text: var(--contrast-500);}
Changelog
2025-05-01
- Added
[&>div>*:last-child]:mb-0
toproduct.description
to remove defaultmargin-bottom
added byprose
for the last child - Added
[&>div>*:first-child]:mt-0
toproduct.description
to remove defaultmargin-top
added byprose
for the first child