diff --git a/.gitignore b/.gitignore index 2778e7ae..b8afb86f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ yarn-error.log* .idea .vscode .cursor +agent-os # Local history .lh diff --git a/packages/ui/src/components/ProductCarousel/ProductCarousel.stories.tsx b/packages/ui/src/components/ProductCarousel/ProductCarousel.stories.tsx new file mode 100644 index 00000000..6cca242e --- /dev/null +++ b/packages/ui/src/components/ProductCarousel/ProductCarousel.stories.tsx @@ -0,0 +1,175 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; +import React from 'react'; + +import { Models as FrontendModels } from '@o2s/utils.frontend'; + +import { Button } from '@o2s/ui/elements/button'; + +import { ProductCarousel } from './ProductCarousel'; +import { ProductSummaryItem } from './ProductCarousel.types'; + +// Mock LinkComponent for stories +const MockLinkComponent: FrontendModels.Link.LinkComponent = ({ href, className, children }) => ( + + {children} + +); + +const meta = { + title: 'Components/ProductCarousel', + component: ProductCarousel, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +// Sample data +const sampleProducts: ProductSummaryItem[] = [ + { + id: 'PRD-005', + name: 'Cordless Angle Grinder', + description: '

Cordless angle grinder with 22V battery platform

', + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-charger.jpg', + alt: 'Cordless Angle Grinder', + width: 640, + height: 656, + }, + price: { + value: 199.99, + currency: 'USD', + }, + link: '/products/ag-125-a22', + badges: [ + { label: 'New', variant: 'secondary' }, + { label: 'Promo', variant: 'destructive' }, + ], + }, + { + id: 'PRD-006', + name: 'Laser Measurement Device', + description: '

Laser measurement device for distance measurements

', + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/empty.jpg', + alt: 'Laser Measurement', + width: 640, + height: 656, + }, + price: { + value: 100, + currency: 'USD', + }, + link: '/products/pd-s', + badges: [{ label: 'New', variant: 'secondary' }], + }, + { + id: 'PRD-007', + name: 'Cordless Drill Driver', + description: '

Cordless drill driver with 22V battery platform

', + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/empty.jpg', + alt: 'Cordless Drill Driver', + width: 640, + height: 656, + }, + price: { + value: 100, + currency: 'USD', + }, + link: '/products/sfc-22-a', + badges: [{ label: 'New', variant: 'secondary' }], + }, + { + id: 'PRD-008', + name: 'Professional Calibration Service', + description: '

ISO-Certified Calibration for industrial equipment

', + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-calibration.jpg', + alt: 'Professional Calibration Service', + width: 640, + height: 656, + }, + price: { + value: 149.99, + currency: 'USD', + }, + link: '/services/calibration', + badges: [{ label: 'Popular', variant: 'default' }], + }, + { + id: 'PRD-009', + name: 'Safety Equipment Package', + description: '

Complete safety equipment for welding environments

', + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-welding.jpg', + alt: 'Safety Equipment Package', + width: 640, + height: 656, + }, + price: { + value: 299.99, + currency: 'USD', + }, + link: '/products/safety-package', + badges: [ + { label: 'Bestseller', variant: 'default' }, + { label: 'Safety', variant: 'outline' }, + ], + }, + { + id: 'PRD-010', + name: 'Power Tool Battery Pack', + description: '

High-capacity battery pack for cordless tools

', + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-charger.jpg', + alt: 'Power Tool Battery Pack', + width: 640, + height: 656, + }, + price: { + value: 79.99, + currency: 'USD', + }, + link: '/products/battery-pack', + badges: [{ label: 'New', variant: 'secondary' }], + }, +]; + +export const Default: Story = { + args: { + products: sampleProducts, + title: 'Recommended Products', + LinkComponent: MockLinkComponent, + detailsLabel: 'View Details', + }, +}; + +export const WithDescription: Story = { + args: { + products: sampleProducts, + title: 'You Might Also Like', + description: '

Check out these carefully selected products that complement your choice.

', + LinkComponent: MockLinkComponent, + detailsLabel: 'View Details', + }, +}; + +export const WithAction: Story = { + args: { + products: sampleProducts, + title: 'Popular Products', + description: '

Discover our most popular items chosen by customers like you.

', + action: ( + + ), + LinkComponent: MockLinkComponent, + detailsLabel: 'View Details', + }, +}; diff --git a/packages/ui/src/components/ProductCarousel/ProductCarousel.tsx b/packages/ui/src/components/ProductCarousel/ProductCarousel.tsx new file mode 100644 index 00000000..478399ee --- /dev/null +++ b/packages/ui/src/components/ProductCarousel/ProductCarousel.tsx @@ -0,0 +1,88 @@ +'use client'; + +import React from 'react'; + +import { cn } from '@o2s/ui/lib/utils'; + +import { ProductCard } from '@o2s/ui/components/Cards/ProductCard'; +import { Carousel } from '@o2s/ui/components/Carousel'; +import { RichText } from '@o2s/ui/components/RichText'; + +import { Typography } from '@o2s/ui/elements/typography'; + +import { ProductCarouselProps } from './ProductCarousel.types'; + +export const ProductCarousel: React.FC = ({ + products, + title, + description, + action, + LinkComponent, + carouselConfig, + detailsLabel, + carouselClassName, +}) => { + if (!products || products.length === 0) { + return null; + } + + return ( +
+ {/* Header section */} + {(title || description || action) && ( +
+ {(title || description) && ( +
+ {title && {title}} + {description && } +
+ )} + {action} +
+ )} + + {/* Carousel */} + ( +
+ +
+ ))} + slidesPerView={1} + spaceBetween={16} + showNavigation={true} + showPagination={true} + className={cn( + '[&_.swiper-slide]:h-auto [&_.swiper-pagination]:bottom-0 [&_.swiper-pagination-bullet]:bg-primary [&_.swiper-pagination-bullet-active]:bg-primary [&_.swiper-pagination-bullet]:opacity-100 [&_.swiper-pagination-bullet]:w-2.5 [&_.swiper-pagination-bullet]:h-2.5', + carouselClassName, + )} + breakpoints={{ + 0: { + slidesPerView: 1, + spaceBetween: 16, + }, + 640: { + slidesPerView: 2, + spaceBetween: 20, + }, + 1024: { + slidesPerView: 3, + spaceBetween: 24, + }, + }} + {...carouselConfig} + /> +
+ ); +}; diff --git a/packages/ui/src/components/ProductCarousel/ProductCarousel.types.ts b/packages/ui/src/components/ProductCarousel/ProductCarousel.types.ts new file mode 100644 index 00000000..5cdfac05 --- /dev/null +++ b/packages/ui/src/components/ProductCarousel/ProductCarousel.types.ts @@ -0,0 +1,32 @@ +import { Models } from '@o2s/framework/modules'; +import React from 'react'; + +import { Models as FrontendModels } from '@o2s/utils.frontend'; + +import { CarouselProps } from '@o2s/ui/components/Carousel'; + +export interface ProductCarouselProps { + products: ProductSummaryItem[]; + title?: string; + description?: Models.RichText.RichText; + action?: React.ReactNode; + LinkComponent: FrontendModels.Link.LinkComponent; + carouselConfig?: Partial; + detailsLabel: string; + carouselClassName?: string; +} + +export interface ProductSummaryItem { + id: string; + name: string; + description?: Models.RichText.RichText; + image?: Models.Media.Media; + price?: Models.Price.Price; + link: string; + badges?: ProductBadge[]; +} + +export interface ProductBadge { + label: string; + variant: 'default' | 'secondary' | 'destructive' | 'outline'; +} diff --git a/packages/ui/src/components/ProductCarousel/index.ts b/packages/ui/src/components/ProductCarousel/index.ts new file mode 100644 index 00000000..a63d35bd --- /dev/null +++ b/packages/ui/src/components/ProductCarousel/index.ts @@ -0,0 +1,2 @@ +export { ProductCarousel } from './ProductCarousel'; +export type { ProductCarouselProps, ProductSummaryItem, ProductBadge } from './ProductCarousel.types';