Order Details Section
Installation
Add the following Soul components
The order-details-section component uses the badge component. Make sure you have added it to your project.
Install the following dependencies
npm install lucide-react
Copy and paste the following code into your project
sections/order-details-section/index.tsx
import { ArrowLeft } from 'lucide-react';import Image from 'next/image';import Link from 'next/link';import { Badge } from '@/vibes/soul/primitives/badge';interface Summary { lineItems: Array<{ label: string; value: string; subtext?: string; }>; total: string;}interface Address { name?: string; street1: string; street2?: string; city: string; state: string; zipcode?: string; country?: string;}interface TrackingWithUrl { url: string;}interface TrackingWithNumber { number: string;}interface TrackingWithUrlAndNumber { url: string; number: string;}interface Shipment { name: string; status: string; tracking?: TrackingWithUrl | TrackingWithNumber | TrackingWithUrlAndNumber;}interface ShipmentLineItem { id: string; title: string; subtitle?: string; price: string; href: string; image?: { src: string; alt: string }; quantity: number; metadata?: Array<{ label: string; value: string }>;}interface Destination { id: string; title: string; address: Address; shipments: Shipment[]; lineItems: ShipmentLineItem[];}export interface Order { id: string; status: string; statusColor?: 'success' | 'warning' | 'error' | 'info'; date: string; destinations: Destination[]; summary: Summary;}export interface OrderDetailsSectionProps { order: Order; title?: string; shipmentAddressLabel?: string; shipmentMethodLabel?: string; summaryTotalLabel?: string; prevHref?: string;}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { * --order-details-section-focus: hsl(var(--primary)); * --order-details-section-font-family: hsl(var(--font-family-body)); * --order-details-section-title-font-family: hsl(var(--font-family-heading)); * --order-details-text-primary: hsl(var(--foreground)); * --order-details-text-secondary: hsl(var(--contrast-500)); * --order-details-section-border: hsl(var(--contrast-100)); * --order-details-section-button-border: hsl(var(--contrast-100)); * --order-details-section-button-border-hover: hsl(var(--contrast-200)); * --order-details-section-button-icon: hsl(var(--foreground)); * --order-details-section-button-background: hsl(var(--background)); * --order-details-section-button-background-hover: hsl(var(--contrast-100)); * --order-details-section-image-background: hsl(var(--contrast-100)); * --order-details-section-line-item: hsl(var(--contrast-300)) * --order-details-section-line-item-subtitle: hsl(var(--contrast-500)) * --order-details-section-line-item-subtext: hsl(var(--contrast-400)) * } * ``` */export function OrderDetailsSection({ order, title = `Order #${order.id}`, shipmentAddressLabel, shipmentMethodLabel, summaryTotalLabel, prevHref = '/orders',}: OrderDetailsSectionProps) { return ( <div className="font-[family-name:var(--order-details-section-font-family,var(--font-family-body))] text-[var(--order-details-text-primary,hsl(var(--foreground)))] @container"> <div className="flex gap-4 border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] pb-8"> <Link className="mt-1 flex h-12 w-12 items-center justify-center rounded-full border border-[var(--order-details-section-button-border,hsl(var(--contrast-100)))] bg-[var(--order-details-section-button-background,hsl(var(--background)))] text-[var(--order-details-section-button-icon,hsl(var(--foreground)))] ring-[var(--order-details-section-focus,hsl(var(--primary)))] transition-colors duration-300 hover:border-[var(--order-details-section-button-border-hover,hsl(var(--contrast-200)))] hover:bg-[var(--order-details-section-button-background-hover,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2" href={prevHref} > <ArrowLeft /> </Link> <div className="space-y-1"> <div className="flex items-center gap-3"> <h1 className="font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-4xl"> {title} </h1> <Badge variant={order.statusColor}>{order.status}</Badge> </div> <p className="text-base font-light">{order.date}</p> </div> </div> <div className="grid @3xl:flex"> <div className="order-2 flex-1 pr-12 @3xl:order-1"> {order.destinations.map((destination) => ( <Shipment addressLabel={shipmentAddressLabel} destination={destination} key={destination.id} methodLabel={shipmentMethodLabel} /> ))} </div> <div className="order-1 basis-72 pt-8 @3xl:order-2"> <div className="font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-2xl font-medium"> Order summary </div> <Summary summary={order.summary} totalLabel={summaryTotalLabel} /> </div> </div> </div> );}function Shipment({ destination, addressLabel = 'Shipping address', methodLabel = 'Shipping method',}: { destination: Destination; addressLabel?: string; methodLabel?: string;}) { return ( <div className="border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-8 @container"> <div className="space-y-6"> <div className="font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-2xl font-medium"> {destination.title} </div> <div className="grid gap-8 @xl:flex @xl:gap-20"> <div className="text-sm"> <h3 className="font-semibold">{addressLabel}</h3> <div className="text-[var(--order-details-text-secondary,hsl(var(--contrast-500)))]"> <p>{destination.address.name}</p> <p>{destination.address.street1}</p> <p>{destination.address.street2}</p> <p> {`${destination.address.city}, ${destination.address.state} ${destination.address.zipcode}`} </p> <p>{destination.address.country}</p> </div> </div> {destination.shipments.map((shipment) => ( <div className="text-sm" key={shipment.name}> <h3 className="font-semibold">{methodLabel}</h3> <div className="text-[var(--order-details-text-secondary,hsl(var(--contrast-500)))]"> <p>{shipment.name}</p> <p>{shipment.status}</p> <ShipmentTracking tracking={shipment.tracking} /> </div> </div> ))} </div> {destination.lineItems.map((lineItem) => ( <ShipmentLineItem key={lineItem.id} lineItem={lineItem} /> ))} </div> </div> );}function ShipmentTracking({ tracking,}: { tracking?: TrackingWithUrl | TrackingWithNumber | TrackingWithUrlAndNumber;}) { if (!tracking) { return null; } if ('url' in tracking && 'number' in tracking) { return ( <p> <Link href={tracking.url} target="_blank"> {tracking.number} </Link> </p> ); } if ('url' in tracking) { return ( <p> <Link href={tracking.url} target="_blank"> {tracking.url} </Link> </p> ); } return <p>{tracking.number}</p>;}function ShipmentLineItem({ lineItem }: { lineItem: ShipmentLineItem }) { return ( <Link className="group grid shrink-0 cursor-pointer gap-8 rounded-xl ring-[var(--order-details-section-focus,hsl(var(--primary)))] ring-offset-4 focus-visible:outline-none focus-visible:ring-2 @sm:flex @sm:rounded-2xl" href={lineItem.href} id={lineItem.id} > <div className="relative aspect-square basis-40 overflow-hidden rounded-[inherit] border border-[var(--order-details-section-border,hsl(var(--contrast-100)))] bg-[var(--order-details-section-image-background,hsl(var(--contrast-100)))]"> {lineItem.image?.src != null ? ( <Image alt={lineItem.image.alt} className="w-full scale-100 select-none object-cover transition-transform duration-500 ease-out group-hover:scale-110" fill sizes="10rem" src={lineItem.image.src} /> ) : ( <div className="pl-2 pt-3 text-4xl font-bold leading-[0.8] tracking-tighter text-[var(--order-details-section-line-item,hsl(var(--contrast-300)))] transition-transform duration-500 ease-out group-hover:scale-105"> {lineItem.title} </div> )} </div> <div className="space-y-3 text-sm leading-snug"> <div> <div className="font-semibold">{lineItem.title}</div> {lineItem.subtitle != null && lineItem.subtitle !== '' && ( <div className="font-normal text-[var(--order-details-section-line-item-subtitle,hsl(var(--contrast-500)))]"> {lineItem.subtitle} </div> )} </div> <div className="flex gap-1 text-sm"> <span className="font-semibold">{lineItem.price}</span> <span>×</span> <span className="font-semibold">{lineItem.quantity}</span> </div> <div> {lineItem.metadata?.map((metadata, index) => ( <div className="flex gap-1 text-sm" key={index}> <span className="font-semibold">{metadata.label}:</span> <span>{metadata.value}</span> </div> ))} </div> </div> </Link> );}function Summary({ summary, totalLabel = 'Total' }: { summary: Summary; totalLabel?: string }) { return ( <div> <div className="space-y-2 pb-3 pt-5"> {summary.lineItems.map((lineItem, index) => ( <div className="flex justify-between" key={index}> <div> <div className="text-sm">{lineItem.label}</div> {lineItem.subtext != null && lineItem.subtext !== '' && ( <div className="text-xs text-[var(--order-details-section-line-item-subtext,hsl(var(--contrast-400)))]"> {lineItem.subtext} </div> )} </div> <span className="text-sm">{lineItem.value}</span> </div> ))} </div> <div className="flex justify-between border-t border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-3 font-semibold"> <span>{totalLabel}</span> <span>{summary.total}</span> </div> </div> );}
Usage
import { OrderDetailsSection } from '@/vibes/soul/sections/order-details-section';function Usage() { return ( <OrderDetailsSection order={order} prevHref="#" /> );}const lineItems = [ { id: '1', title: 'Mini Bar Bag', subtitle: 'Blue/Black/Green', price: '$65', image: { src: 'https://rstr.in/monogram/vibes/mrlTNE1TJfB', alt: 'Mini Bar Bag', }, href: '#', rating: 4.3, quantity: 1, }, { id: '2', title: 'Mini Bar Bag', subtitle: 'Blue/Black/Green', price: '$65', image: { src: 'https://rstr.in/monogram/vibes/LznMEk1GSB1', alt: 'Mini Bar Bag', }, href: '#', rating: 4.5, quantity: 2, }];const destination1 = { id: '1', title: 'Destination 1/2', address: { name: 'John Doe', street1: '1000 San Marcos Ave', city: 'Austin', state: 'TX', zipcode: '78702', country: 'United States', }, shipments: [ { tracking: { number: '1Z370170375602560', }, name: 'UPS Ground', status: 'Delivered on May 15, 2024', }, ], lineItems: lineItems.slice(0, 3),};const subtotal = [destination1, destination2].reduce((acc, destination) => { return ( acc + destination.lineItems.reduce( (accInner, lineItem) => accInner + parseInt(lineItem.price.slice(1), 10) * lineItem.quantity, 0, ) );}, 0);const discount = 10;const shipping = 20;const tax = subtotal * 0.08;const total = subtotal - discount + shipping + tax;const order = { id: '1', status: 'Delivered', statusColor: 'success' as const, date: '2021-01-01', destinations: [destination1], summary: { lineItems: [ { label: 'Subtotal', value: "$" + subtotal.toFixed(2) }, { label: 'Discount', value: "-$" + discount.toFixed(2) }, { label: 'Shipping', value: "$" + shipping.toFixed(2), subtext: 'Ground' }, { label: 'Tax', value: "$" + tax.toFixed(2) }, ], total: "$" + total.toFixed(2), },};
API Reference
OrderDetailsSectionProps
Prop | Type | Default |
---|---|---|
order* | Order | |
title | string | Order # |
shipmentAddressLabel | string | |
shipmentMethodLabel | string | |
summaryTotalLabel | string | |
prevHref | string | '/orders' |
Order
Prop | Type | Default |
---|---|---|
id* | string | |
status* | string | |
statusColor? | 'success' | 'warning' | 'error' | 'info' | |
date* | string | |
destinations* | Destination[] | |
summary* | Summary |
Destination
Prop | Type | Default |
---|---|---|
id* | string | |
title* | string | |
address* | Address | |
shipments* | Shipment[] | |
lineItems* | ShipmentLineItem[] |
Summary
Prop | Type | Default |
---|---|---|
lineItems* | Array<{label: string; value: string; subtext?: string; }> | |
total* | string |
Address
Prop | Type | Default |
---|---|---|
name | string | |
street1* | string | |
street2 | string | |
city* | string | |
state* | string | |
zipcode | string | |
country | string |
Shipment
Prop | Type | Default |
---|---|---|
name* | string | |
status* | string | |
tracking | TrackingWithUrl | TrackingWithNumber | TrackingWithUrlAndNumber |
ShipmentLineItem
Prop | Type | Default |
---|---|---|
id* | string | |
title* | string | |
subtitle | string | |
price* | string | |
href* | string | |
image | { src: string; alt: string } | |
quantity* | number | |
metadata | Array<{ label: string; value: string }> |
TrackingWithUrl
Prop | Type | Default |
---|---|---|
url* | string |
TrackingWithNumber
Prop | Type | Default |
---|---|---|
number* | string |
TrackingWithUrlAndNumber
Prop | Type | Default |
---|---|---|
url* | string | |
number* | string |
CSS Variables
This component supports various CSS variables for theming. Here's a comprehensive list.
:root { --order-details-section-focus: hsl(var(--primary)); --order-details-section-font-family: hsl(var(--font-family-body)); --order-details-section-title-font-family: hsl(var(--font-family-heading)); --order-details-text-primary: hsl(var(--foreground)); --order-details-text-secondary: hsl(var(--contrast-500)); --order-details-section-border: hsl(var(--contrast-100)); --order-details-section-button-border: hsl(var(--contrast-100)); --order-details-section-button-border-hover: hsl(var(--contrast-200)); --order-details-section-button-icon: hsl(var(--foreground)); --order-details-section-button-background: hsl(var(--background)); --order-details-section-button-background-hover: hsl(var(--contrast-100)); --order-details-section-image-background: hsl(var(--contrast-100)); --order-details-section-line-item: hsl(var(--contrast-300)); --order-details-section-line-item-subtitle: hsl(var(--contrast-500)); --order-details-section-line-item-subtext: hsl(var(--contrast-400));}