-
Notifications
You must be signed in to change notification settings - Fork 205
DRAFT - DO NOT MERGE - TEST PR ONLY #3551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
szirpesf
wants to merge
1
commit into
SalesforceCommerceCloud:develop
Choose a base branch
from
szirpesf:W-20727098-test-pr-agent
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
packages/template-retail-react-app/PRODUCT_COMPARISON.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| # Product Comparison Feature | ||
|
|
||
| This document describes the product comparison functionality added to the Retail React App. | ||
|
|
||
| ## Overview | ||
|
|
||
| The product comparison feature allows users to: | ||
| - Add up to 4 products to a comparison list | ||
| - View compared products in a side drawer | ||
| - Navigate to a detailed comparison page | ||
| - Compare product attributes side by side | ||
| - Persist comparison data across browser sessions | ||
|
|
||
| ## Components | ||
|
|
||
| ### Core Components | ||
|
|
||
| 1. **ComparisonProvider** (`app/contexts/comparison-provider.jsx`) | ||
| - Manages global comparison state | ||
| - Handles localStorage persistence | ||
| - Provides comparison context to the app | ||
|
|
||
| 2. **useComparison Hook** (`app/hooks/use-comparison.js`) | ||
| - Convenient hook for accessing comparison functionality | ||
| - Provides methods for adding/removing products | ||
| - Manages drawer state | ||
|
|
||
| 3. **CompareButton** (`app/components/compare-button/index.jsx`) | ||
| - Reusable button component for adding/removing products from comparison | ||
| - Available in icon and button variants | ||
| - Provides user feedback via toasts | ||
|
|
||
| 4. **ComparisonDrawer** (`app/components/comparison-drawer/index.jsx`) | ||
| - Side drawer showing currently compared products | ||
| - Quick access to remove products | ||
| - Navigate to full comparison page | ||
|
|
||
| 5. **ComparisonBadge** (`app/components/comparison-badge/index.jsx`) | ||
| - Floating badge showing comparison count | ||
| - Quick access to open comparison drawer | ||
|
|
||
| 6. **ProductComparison Page** (`app/pages/product-comparison/index.jsx`) | ||
| - Full comparison page with detailed product table | ||
| - Responsive design (table on desktop, cards on mobile) | ||
| - Compare product attributes side by side | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Enabling Comparison on Product Tiles | ||
|
|
||
| ```jsx | ||
| <ProductTile | ||
| product={product} | ||
| enableComparison={true} | ||
| // ... other props | ||
| /> | ||
| ``` | ||
|
|
||
| ### Using the Comparison Hook | ||
|
|
||
| ```jsx | ||
| import {useComparison} from '@salesforce/retail-react-app/app/hooks' | ||
|
|
||
| function MyComponent() { | ||
| const { | ||
| comparedProducts, | ||
| addToComparison, | ||
| removeFromComparison, | ||
| isInComparison, | ||
| openDrawer | ||
| } = useComparison() | ||
|
|
||
| // Component logic... | ||
| } | ||
| ``` | ||
|
|
||
| ### Adding Compare Button | ||
|
|
||
| ```jsx | ||
| import CompareButton from '@salesforce/retail-react-app/app/components/compare-button' | ||
|
|
||
| <CompareButton | ||
| product={product} | ||
| variant="button" // or "icon" | ||
| size="md" | ||
| /> | ||
| ``` | ||
|
|
||
| ## Features | ||
|
|
||
| ### State Management | ||
| - Global state managed by ComparisonProvider | ||
| - Automatic persistence to localStorage | ||
| - Maximum of 4 products can be compared | ||
| - Duplicate prevention | ||
|
|
||
| ### User Experience | ||
| - Toast notifications for user feedback | ||
| - Floating comparison badge for quick access | ||
| - Side drawer for quick product management | ||
| - Responsive comparison page | ||
|
|
||
| ### Accessibility | ||
| - Proper ARIA labels | ||
| - Keyboard navigation support | ||
| - Screen reader friendly | ||
|
|
||
| ## API | ||
|
|
||
| ### ComparisonProvider Props | ||
| - `children`: React children to wrap | ||
|
|
||
| ### useComparison Return Value | ||
| - `comparedProducts`: Array of products being compared | ||
| - `isDrawerOpen`: Boolean indicating drawer state | ||
| - `addToComparison(product)`: Add product to comparison | ||
| - `removeFromComparison(productId)`: Remove product from comparison | ||
| - `clearComparison()`: Clear all compared products | ||
| - `isInComparison(productId)`: Check if product is being compared | ||
| - `toggleDrawer()`: Toggle drawer open/closed | ||
| - `openDrawer()`: Open comparison drawer | ||
| - `closeDrawer()`: Close comparison drawer | ||
| - `canCompare`: Boolean indicating if more products can be added | ||
| - `hasProducts`: Boolean indicating if there are products to compare | ||
| - `count`: Number of products currently being compared | ||
|
|
||
| ### CompareButton Props | ||
| - `product`: Product object (required) | ||
| - `variant`: "icon" | "button" (default: "icon") | ||
| - `size`: "sm" | "md" | "lg" (default: "md") | ||
|
|
||
| ## Testing | ||
|
|
||
| Unit tests are provided for: | ||
| - useComparison hook functionality | ||
| - CompareButton component behavior | ||
| - State management and persistence | ||
|
|
||
| Run tests with: | ||
| ```bash | ||
| npm test -- --testPathPattern=comparison | ||
| ``` | ||
|
|
||
| ## Routing | ||
|
|
||
| The comparison page is available at `/compare` and will redirect to home if no products are selected for comparison. | ||
|
|
||
| ## Localization | ||
|
|
||
| All user-facing text is internationalized using react-intl. Add translations for: | ||
| - `comparison_drawer.*` | ||
| - `compare_button.*` | ||
| - `product_comparison.*` | ||
| - `comparison_badge.*` | ||
|
|
||
| ## Browser Support | ||
|
|
||
| The feature uses localStorage for persistence and falls back gracefully if not available. Supports all modern browsers that support the base PWA Kit requirements. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
190 changes: 190 additions & 0 deletions
190
packages/template-retail-react-app/app/components/compare-button/index.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| /* | ||
| * Copyright (c) 2024, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
|
|
||
| import React from 'react' | ||
| import PropTypes from 'prop-types' | ||
| import {useIntl} from 'react-intl' | ||
| import { | ||
| IconButton, | ||
| Button, | ||
| Tooltip, | ||
| useMultiStyleConfig | ||
| } from '@salesforce/retail-react-app/app/components/shared/ui' | ||
| import {useComparison} from '@salesforce/retail-react-app/app/hooks' | ||
| import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast' | ||
|
|
||
| // Icons - we'll create a simple compare icon using existing UI components | ||
| const CompareIcon = () => ( | ||
| <svg | ||
| width="20" | ||
| height="20" | ||
| viewBox="0 0 24 24" | ||
| fill="currentColor" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <path d="M9 3H7c-1.1 0-2 .9-2 2v9h2V5h2V3zm4 6V7l-3 3 3 3v-2h4v2l3-3-3-3v2h-4zm2 8h2v-7h2v7c0 1.1-.9 2-2 2h-2v-2z"/> | ||
| </svg> | ||
| ) | ||
|
|
||
| /** | ||
| * CompareButton component allows users to add/remove products from comparison. | ||
| * Can be rendered as an icon button or regular button. | ||
| */ | ||
| const CompareButton = ({ | ||
| product, | ||
| variant = 'icon', | ||
| size = 'md', | ||
| ...rest | ||
| }) => { | ||
| const intl = useIntl() | ||
| const toast = useToast() | ||
| const { | ||
| addToComparison, | ||
| removeFromComparison, | ||
| isInComparison, | ||
| canCompare | ||
| } = useComparison() | ||
|
|
||
| const isComparing = isInComparison(product.productId) | ||
| const styles = useMultiStyleConfig('CompareButton', {variant, size}) | ||
|
|
||
| // ProductTile is used by two components, RecommendedProducts and ProductList. | ||
| // RecommendedProducts provides a localized product name as `name` and non-localized product | ||
| // name as `productName`. ProductList provides a localized name as `productName` and does not | ||
| // use the `name` property. | ||
| const localizedProductName = product.name ?? product.productName | ||
|
|
||
| const handleClick = async (e) => { | ||
| e.preventDefault() | ||
| e.stopPropagation() | ||
|
|
||
| try { | ||
| if (isComparing) { | ||
| removeFromComparison(product.productId) | ||
| toast({ | ||
| title: intl.formatMessage( | ||
| { | ||
| id: 'compare_button.removed_from_comparison', | ||
| defaultMessage: '{product} removed from comparison' | ||
| }, | ||
| {product: localizedProductName} | ||
| ), | ||
| status: 'info', | ||
| duration: 2000 | ||
| }) | ||
| } else { | ||
| if (!canCompare) { | ||
| toast({ | ||
| title: intl.formatMessage({ | ||
| id: 'compare_button.max_products_reached', | ||
| defaultMessage: 'Maximum 4 products can be compared at once' | ||
| }), | ||
| status: 'warning', | ||
| duration: 3000 | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| addToComparison(product) | ||
| toast({ | ||
| title: intl.formatMessage( | ||
| { | ||
| id: 'compare_button.added_to_comparison', | ||
| defaultMessage: '{product} added to comparison' | ||
| }, | ||
| {product: localizedProductName} | ||
| ), | ||
| status: 'success', | ||
| duration: 2000 | ||
| }) | ||
| } | ||
| } catch (error) { | ||
| toast({ | ||
| title: intl.formatMessage({ | ||
| id: 'compare_button.error', | ||
| defaultMessage: 'Unable to update comparison' | ||
| }), | ||
| description: error.message, | ||
| status: 'error', | ||
| duration: 3000 | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| const ariaLabel = isComparing | ||
| ? intl.formatMessage( | ||
| { | ||
| id: 'compare_button.remove_from_comparison', | ||
| defaultMessage: 'Remove {product} from comparison' | ||
| }, | ||
| {product: localizedProductName} | ||
| ) | ||
| : intl.formatMessage( | ||
| { | ||
| id: 'compare_button.add_to_comparison', | ||
| defaultMessage: 'Add {product} to comparison' | ||
| }, | ||
| {product: localizedProductName} | ||
| ) | ||
|
|
||
| const buttonText = isComparing | ||
| ? intl.formatMessage({ | ||
| id: 'compare_button.comparing', | ||
| defaultMessage: 'Comparing' | ||
| }) | ||
| : intl.formatMessage({ | ||
| id: 'compare_button.compare', | ||
| defaultMessage: 'Compare' | ||
| }) | ||
|
|
||
| if (variant === 'icon') { | ||
| return ( | ||
| <Tooltip | ||
| label={ariaLabel} | ||
| hasArrow | ||
| placement="top" | ||
| > | ||
| <IconButton | ||
| aria-label={ariaLabel} | ||
| icon={<CompareIcon />} | ||
| onClick={handleClick} | ||
| colorScheme={isComparing ? 'blue' : 'gray'} | ||
| variant={isComparing ? 'solid' : 'ghost'} | ||
| size={size} | ||
| {...styles.container} | ||
| {...rest} | ||
| /> | ||
| </Tooltip> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <Button | ||
| leftIcon={<CompareIcon />} | ||
| onClick={handleClick} | ||
| colorScheme={isComparing ? 'blue' : 'gray'} | ||
| variant={isComparing ? 'solid' : 'outline'} | ||
| size={size} | ||
| {...styles.container} | ||
| {...rest} | ||
| > | ||
| {buttonText} | ||
| </Button> | ||
| ) | ||
| } | ||
|
|
||
| CompareButton.propTypes = { | ||
| product: PropTypes.shape({ | ||
| productId: PropTypes.string.isRequired, | ||
| name: PropTypes.string, | ||
| productName: PropTypes.string | ||
| }).isRequired, | ||
| variant: PropTypes.oneOf(['icon', 'button']), | ||
| size: PropTypes.oneOf(['sm', 'md', 'lg']) | ||
| } | ||
|
|
||
| export default CompareButton | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catch is not needed