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>

PropTypeDefault
breadcrumbs
Streamable<Breadcrumb[]>
product
Streamable<ProductDetailProduct | null>
action
ProductDetailFormAction<F>
fields
quantityLabel
string
incrementLabel
string
decrementLabel
string
ctaLabel
Streamable<string | null>
ctaDisabled
Streamable<boolean | null>
prefetch
boolean
thumbnailLabel
string
additionalInformationTitle
string

ProductDetailProduct

PropTypeDefault
id
string
title
string
href
string
images
Streamable<Image[]>
price
Streamable<string | PriceRange | PriceSale | null>
subtitle
string
badge
string
rating
Streamable<number | null>
summary
Streamable<string>
description
Streamable<string | ReactNode | null>
accordions
Streamable<Accordion[]>

Acorrdion

PropTypeDefault
title*
string
content*
ReactNode

Image

PropTypeDefault
src*
string
alt*
string

PriceRange

PropTypeDefault
type
'range'
minValue
string
maxValue
string

PriceSale

PropTypeDefault
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!',  };}
PropTypeDefault
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 to product.description to remove default margin-bottom added by prose for the last child
  • Added [&>div>*:first-child]:mt-0 to product.description to remove default margin-top added by prose for the first child