@asantemedia-org/atlas-copco-vt-storybook 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierignore +2 -0
- package/.prettierrc +8 -0
- package/.storybook/AtlasCopcoTheme.ts +14 -0
- package/.storybook/global.scss +15 -0
- package/.storybook/main.ts +125 -0
- package/.storybook/manager.ts +6 -0
- package/.storybook/preview-head.html +4 -0
- package/.storybook/preview.tsx +73 -0
- package/.storybook/types.d.ts +5 -0
- package/.vscode/settings.json +4 -0
- package/README.md +59 -0
- package/next.config.js +8 -0
- package/package.json +76 -0
- package/postcss.config.js +6 -0
- package/public/.gitkeep +0 -0
- package/public/assets/.gitkeep +0 -0
- package/public/fonts/.gitkeep +0 -0
- package/src/app/globals.css +13 -0
- package/src/app/layout.tsx +18 -0
- package/src/app/page.tsx +11 -0
- package/src/components/Button/Button.module.scss +77 -0
- package/src/components/Button/Button.stories.tsx +97 -0
- package/src/components/Button/Button.tsx +47 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Image/Image.module.scss +75 -0
- package/src/components/Image/Image.tsx +114 -0
- package/src/components/Image/Image.types.ts +34 -0
- package/src/components/Image/index.ts +2 -0
- package/src/components/ProductCardDetails/ProductCardDetails.module.scss +129 -0
- package/src/components/ProductCardDetails/ProductCardDetails.stories.tsx +138 -0
- package/src/components/ProductCardDetails/ProductCardDetails.tsx +61 -0
- package/src/components/ProductCardDetails/index.ts +2 -0
- package/src/components/ProductCardHorizontal/ProductCardHorizontal.module.scss +93 -0
- package/src/components/ProductCardHorizontal/ProductCardHorizontal.stories.tsx +72 -0
- package/src/components/ProductCardHorizontal/ProductCardHorizontal.tsx +50 -0
- package/src/components/ProductCardHorizontal/index.ts +2 -0
- package/src/experience/algolia-dynamic-search/AlgoliaDynamicSearch.scss +135 -0
- package/src/experience/algolia-dynamic-search/AlgoliaDynamicSearch.stories.tsx +109 -0
- package/src/experience/algolia-dynamic-search/AlgoliaDynamicSearch.tsx +67 -0
- package/src/experience/algolia-dynamic-search/index.ts +2 -0
- package/src/experience/qr-form-journey/QrFormJourney.scss +37 -0
- package/src/experience/qr-form-journey/QrFormJourney.stories.tsx +134 -0
- package/src/experience/qr-form-journey/QrFormJourney.tsx +69 -0
- package/src/experience/qr-form-journey/index.ts +2 -0
- package/src/index.ts +19 -0
- package/src/stories/foundation/_components/StoryLayout.tsx +67 -0
- package/src/stories/introduction/Welcome.mdx +36 -0
- package/src/types/buttons.ts +4 -0
- package/src/types/cards.ts +37 -0
- package/src/utils/data/algolia-dynamic-widget-product-data.json +46 -0
- package/src/utils/styles/base.scss +100 -0
- package/src/utils/styles/global.scss +29 -0
- package/src/utils/styles/index.ts +1 -0
- package/src/utils/styles/typography.scss +60 -0
- package/tailwind.config.js +19 -0
- package/tsconfig.json +41 -0
- package/types/@asantemedia-org__edwardsvacuum-design-system.d.ts +8 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import styles from "./Button.module.scss";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant = "primary" | "secondary" | "outlined";
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
variant?: ButtonVariant;
|
|
10
|
+
size?: "small" | "medium" | "large";
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onClick?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
|
|
13
|
+
type?: "button" | "submit" | "reset";
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Button: React.FC<ButtonProps> = ({
|
|
19
|
+
children,
|
|
20
|
+
variant = "primary",
|
|
21
|
+
size = "medium",
|
|
22
|
+
disabled = false,
|
|
23
|
+
onClick,
|
|
24
|
+
type = "button",
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}) => {
|
|
28
|
+
const buttonClasses = classNames(
|
|
29
|
+
styles.button,
|
|
30
|
+
styles[`button--${variant}`],
|
|
31
|
+
styles[`button--${size}`],
|
|
32
|
+
{ [styles["button--disabled"]]: disabled },
|
|
33
|
+
className
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
type={type}
|
|
39
|
+
className={buttonClasses}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.imageContainer {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: block;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
border-radius: 0.25rem;
|
|
6
|
+
|
|
7
|
+
&.loading {
|
|
8
|
+
.image {
|
|
9
|
+
opacity: 0;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&.error {
|
|
14
|
+
.image {
|
|
15
|
+
display: none;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.image {
|
|
21
|
+
display: block;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
opacity: 1;
|
|
25
|
+
transition: opacity 0.3s ease-in-out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.skeleton {
|
|
29
|
+
position: absolute;
|
|
30
|
+
inset: 0;
|
|
31
|
+
background-color: #e5e7eb;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.shimmer {
|
|
36
|
+
position: absolute;
|
|
37
|
+
inset: 0;
|
|
38
|
+
transform: translateX(-100%);
|
|
39
|
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
|
40
|
+
animation: shimmer 1.5s infinite;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@keyframes shimmer {
|
|
44
|
+
100% {
|
|
45
|
+
transform: translateX(100%);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.errorState {
|
|
50
|
+
position: absolute;
|
|
51
|
+
inset: 0;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
gap: 0.5rem;
|
|
57
|
+
background-color: #f9fafb;
|
|
58
|
+
color: #9ca3af;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.errorIcon {
|
|
62
|
+
width: 2rem;
|
|
63
|
+
height: 2rem;
|
|
64
|
+
opacity: 0.6;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.errorText {
|
|
68
|
+
font-size: 0.75rem;
|
|
69
|
+
font-weight: 500;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.imageContainer:focus-visible {
|
|
73
|
+
outline: 2px solid #0062b0;
|
|
74
|
+
outline-offset: 2px;
|
|
75
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
|
+
import { ImageProps } from "./Image.types";
|
|
3
|
+
import styles from "./Image.module.scss";
|
|
4
|
+
|
|
5
|
+
export const Image: React.FC<ImageProps> = ({
|
|
6
|
+
src,
|
|
7
|
+
alt,
|
|
8
|
+
width,
|
|
9
|
+
height,
|
|
10
|
+
loading = "lazy",
|
|
11
|
+
decoding = "async",
|
|
12
|
+
objectFit = "cover",
|
|
13
|
+
objectPosition = "center",
|
|
14
|
+
aspectRatio,
|
|
15
|
+
isDecorative = false,
|
|
16
|
+
fallbackSrc,
|
|
17
|
+
onLoad,
|
|
18
|
+
onError,
|
|
19
|
+
className = "",
|
|
20
|
+
srcSet,
|
|
21
|
+
sizes,
|
|
22
|
+
}) => {
|
|
23
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
24
|
+
const [hasError, setHasError] = useState(false);
|
|
25
|
+
const [currentSrc, setCurrentSrc] = useState(src);
|
|
26
|
+
|
|
27
|
+
const handleLoad = useCallback(() => {
|
|
28
|
+
setIsLoading(false);
|
|
29
|
+
setHasError(false);
|
|
30
|
+
onLoad?.();
|
|
31
|
+
}, [onLoad]);
|
|
32
|
+
|
|
33
|
+
const handleError = useCallback(() => {
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
setHasError(true);
|
|
36
|
+
if (fallbackSrc && currentSrc !== fallbackSrc) {
|
|
37
|
+
setCurrentSrc(fallbackSrc);
|
|
38
|
+
setHasError(false);
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
} else {
|
|
41
|
+
onError?.();
|
|
42
|
+
}
|
|
43
|
+
}, [fallbackSrc, currentSrc, onError]);
|
|
44
|
+
|
|
45
|
+
const accessibilityProps = isDecorative
|
|
46
|
+
? { alt: "", "aria-hidden": true as const, role: "presentation" as const }
|
|
47
|
+
: { alt };
|
|
48
|
+
|
|
49
|
+
const containerStyle: React.CSSProperties = {
|
|
50
|
+
...(aspectRatio && { aspectRatio }),
|
|
51
|
+
...(width && { width: typeof width === "number" ? `${width}px` : width }),
|
|
52
|
+
...(height && { height: typeof height === "number" ? `${height}px` : height }),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const imageStyle: React.CSSProperties = {
|
|
56
|
+
objectFit,
|
|
57
|
+
objectPosition,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={`${styles.imageContainer} ${className} ${isLoading ? styles.loading : ""} ${
|
|
63
|
+
hasError ? styles.error : ""
|
|
64
|
+
}`}
|
|
65
|
+
style={containerStyle}
|
|
66
|
+
>
|
|
67
|
+
{isLoading && (
|
|
68
|
+
<div className={styles.skeleton} aria-hidden="true">
|
|
69
|
+
<div className={styles.shimmer} />
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{hasError && !fallbackSrc ? (
|
|
73
|
+
<div className={styles.errorState} role="img" aria-label={alt || "Image failed to load"}>
|
|
74
|
+
<svg
|
|
75
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
76
|
+
viewBox="0 0 24 24"
|
|
77
|
+
fill="none"
|
|
78
|
+
stroke="currentColor"
|
|
79
|
+
strokeWidth="1.5"
|
|
80
|
+
strokeLinecap="round"
|
|
81
|
+
strokeLinejoin="round"
|
|
82
|
+
className={styles.errorIcon}
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
>
|
|
85
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
86
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
87
|
+
<polyline points="21 15 16 10 5 21" />
|
|
88
|
+
</svg>
|
|
89
|
+
<span className={styles.errorText}>Image unavailable</span>
|
|
90
|
+
</div>
|
|
91
|
+
) : (
|
|
92
|
+
// Design system Image uses <img> for portability; next/image not required here
|
|
93
|
+
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
|
|
94
|
+
<img
|
|
95
|
+
src={currentSrc}
|
|
96
|
+
alt={isDecorative ? "" : alt ?? ""}
|
|
97
|
+
{...(isDecorative && { "aria-hidden": true, role: "presentation" })}
|
|
98
|
+
width={width}
|
|
99
|
+
height={height}
|
|
100
|
+
loading={loading}
|
|
101
|
+
decoding={decoding}
|
|
102
|
+
onLoad={handleLoad}
|
|
103
|
+
onError={handleError}
|
|
104
|
+
className={styles.image}
|
|
105
|
+
style={imageStyle}
|
|
106
|
+
srcSet={srcSet}
|
|
107
|
+
sizes={sizes}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default Image;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ImageProps {
|
|
2
|
+
/** Image source URL */
|
|
3
|
+
src: string;
|
|
4
|
+
/** Alt text for accessibility - required for non-decorative images */
|
|
5
|
+
alt: string;
|
|
6
|
+
/** Optional width */
|
|
7
|
+
width?: number | string;
|
|
8
|
+
/** Optional height */
|
|
9
|
+
height?: number | string;
|
|
10
|
+
/** Loading strategy */
|
|
11
|
+
loading?: "lazy" | "eager";
|
|
12
|
+
/** Decoding hint */
|
|
13
|
+
decoding?: "async" | "sync" | "auto";
|
|
14
|
+
/** Object fit behavior */
|
|
15
|
+
objectFit?: "contain" | "cover" | "fill" | "none" | "scale-down";
|
|
16
|
+
/** Object position */
|
|
17
|
+
objectPosition?: string;
|
|
18
|
+
/** Aspect ratio (e.g., "16/9", "4/3", "1/1") */
|
|
19
|
+
aspectRatio?: string;
|
|
20
|
+
/** Whether this is a decorative image (sets alt="" and aria-hidden) */
|
|
21
|
+
isDecorative?: boolean;
|
|
22
|
+
/** Fallback image URL if src fails to load */
|
|
23
|
+
fallbackSrc?: string;
|
|
24
|
+
/** Callback when image loads successfully */
|
|
25
|
+
onLoad?: () => void;
|
|
26
|
+
/** Callback when image fails to load */
|
|
27
|
+
onError?: () => void;
|
|
28
|
+
/** Additional CSS class */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Responsive image srcset */
|
|
31
|
+
srcSet?: string;
|
|
32
|
+
/** Responsive image sizes */
|
|
33
|
+
sizes?: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
@use "../../utils/styles/base.scss" as *;
|
|
2
|
+
|
|
3
|
+
.productDetails {
|
|
4
|
+
:global(.cmp-card--type-product_details) {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-template-columns: repeat(1, 1fr);
|
|
7
|
+
grid-template-rows: auto;
|
|
8
|
+
@include respond-to($breakpoint-md) {
|
|
9
|
+
grid-template-columns: repeat(2, 1fr);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:global(.cmp-card--type-product_details__product-info) {
|
|
14
|
+
position: relative;
|
|
15
|
+
max-width: 480px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:global(.cmp-card--type-product_details__header) {
|
|
19
|
+
margin-bottom: 1rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:global(.cmp-card--type-product_details__header__title) {
|
|
23
|
+
font-size: 21px;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
margin-bottom: 1rem;
|
|
26
|
+
text-transform: uppercase;
|
|
27
|
+
color: $primary-color;
|
|
28
|
+
@media (min-width: 768px) {
|
|
29
|
+
max-width: 325px;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:global(.cmp-card--type-product_details__header__product-code) {
|
|
34
|
+
font-size: 0.875rem;
|
|
35
|
+
color: $text-secondary;
|
|
36
|
+
margin-bottom: 1rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:global(.cmp-card--type-product_details__header__product-image) {
|
|
40
|
+
max-width: 100%;
|
|
41
|
+
height: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:global(.cmp-card--type-product_details__spares-list) {
|
|
45
|
+
position: relative;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:global(.folding-spares-list) {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: repeat(1, 1fr);
|
|
51
|
+
grid-template-rows: auto;
|
|
52
|
+
gap: 10px;
|
|
53
|
+
margin-left: 0;
|
|
54
|
+
padding-left: 0;
|
|
55
|
+
@include respond-to($breakpoint-md) {
|
|
56
|
+
grid-template-columns: repeat(2, 1fr);
|
|
57
|
+
}
|
|
58
|
+
li {
|
|
59
|
+
list-style: none;
|
|
60
|
+
margin-bottom: 0;
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:global(.folding-specification-list) {
|
|
66
|
+
margin-left: 0;
|
|
67
|
+
padding-left: 0;
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-template-columns: repeat(1, 1fr);
|
|
70
|
+
grid-template-rows: auto;
|
|
71
|
+
gap: 10px;
|
|
72
|
+
@include respond-to($breakpoint-md) {
|
|
73
|
+
grid-template-columns: repeat(2, 1fr);
|
|
74
|
+
grid-template-areas: "list-item-1 list-item-2";
|
|
75
|
+
}
|
|
76
|
+
li {
|
|
77
|
+
list-style: none;
|
|
78
|
+
margin-bottom: 0.75rem;
|
|
79
|
+
font-size: 14px;
|
|
80
|
+
strong {
|
|
81
|
+
color: $atlascopco-grey-600;
|
|
82
|
+
}
|
|
83
|
+
& > *:first-child {
|
|
84
|
+
line-height: 2;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:global(.folding-specification-holder) {
|
|
90
|
+
margin-bottom: 1rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
:global(.folding-spares-holder) {
|
|
94
|
+
margin-bottom: 1rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:global(.accordion-section) {
|
|
98
|
+
margin-bottom: 0.5rem;
|
|
99
|
+
&.open {
|
|
100
|
+
border-color: $primary-color;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:global(.accordion-section__header) {
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
align-items: center;
|
|
108
|
+
padding: 1rem 0;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
:global(.accordion-section__title) {
|
|
113
|
+
font-size: 1rem;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
color: $atlascopco-grey-600;
|
|
116
|
+
margin: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:global(.accordion-section__icon) {
|
|
120
|
+
color: $text-primary !important;
|
|
121
|
+
svg {
|
|
122
|
+
color: inherit !important;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:global(.accordion-section__content) {
|
|
127
|
+
padding: 0 0 1rem 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ProductCardDetails } from "./ProductCardDetails";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ProductCardDetails> = {
|
|
5
|
+
title: "Components/Cards/ProductCardDetails",
|
|
6
|
+
component: ProductCardDetails,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "padded",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof ProductCardDetails>;
|
|
15
|
+
|
|
16
|
+
const sampleImage = "https://via.placeholder.com/400x300?text=Atlas+Copco+Product";
|
|
17
|
+
|
|
18
|
+
const sampleHit = {
|
|
19
|
+
objectID: "U20084880",
|
|
20
|
+
code: "U20084880",
|
|
21
|
+
name: "Pump Kit And Checklist",
|
|
22
|
+
title: "Pump Kit And Checklist",
|
|
23
|
+
image: sampleImage,
|
|
24
|
+
description:
|
|
25
|
+
"<h2>Kits</h2><p>Choose genuine Atlas Copco service kits for reliable operation of pumps when properly fitted.</p><ul><li>Genuine parts</li><li>Complete kit contents</li><li>Quality assured</li></ul>",
|
|
26
|
+
price: "£49.99",
|
|
27
|
+
priceValue: "0-49.99",
|
|
28
|
+
itemtype: "Spare Part",
|
|
29
|
+
powerTotalRated: "N/A",
|
|
30
|
+
stockLevelStatus: "In Stock",
|
|
31
|
+
url: "#",
|
|
32
|
+
spares: [
|
|
33
|
+
{
|
|
34
|
+
objectID: "spare-1",
|
|
35
|
+
code: "SP001",
|
|
36
|
+
name: "O-Ring Kit",
|
|
37
|
+
"img-product": sampleImage,
|
|
38
|
+
priceValue: "£12.99",
|
|
39
|
+
url: "#",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
objectID: "spare-2",
|
|
43
|
+
code: "SP002",
|
|
44
|
+
name: "Gasket Set",
|
|
45
|
+
"img-product": sampleImage,
|
|
46
|
+
priceValue: "£24.99",
|
|
47
|
+
url: "#",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
objectID: "spare-3",
|
|
51
|
+
code: "SP003",
|
|
52
|
+
name: "Bearing Assembly",
|
|
53
|
+
"img-product": sampleImage,
|
|
54
|
+
priceValue: "£89.99",
|
|
55
|
+
url: "#",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const sampleFacets = [
|
|
61
|
+
{ name: "itemtype", label: "Item Type" },
|
|
62
|
+
{ name: "powerTotalRated", label: "Power Total Rated" },
|
|
63
|
+
{ name: "stockLevelStatus", label: "Stock Level Status" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export const Default: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
title: "Pump Kit And Checklist",
|
|
69
|
+
imageUrl: sampleImage,
|
|
70
|
+
code: "U20084880",
|
|
71
|
+
hit: sampleHit,
|
|
72
|
+
facets: sampleFacets,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WithSpares: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
title: "GA 30+ - Oil-injected rotary screw compressor",
|
|
79
|
+
imageUrl: sampleImage,
|
|
80
|
+
code: "GA30-VT",
|
|
81
|
+
hit: {
|
|
82
|
+
...sampleHit,
|
|
83
|
+
title: "GA 30+ - Oil-injected rotary screw compressor",
|
|
84
|
+
code: "GA30-VT",
|
|
85
|
+
description:
|
|
86
|
+
"<h2>Industrial Compressor</h2><p>The GA 30+ series offers reliable performance for demanding industrial applications.</p>",
|
|
87
|
+
itemtype: "Compressor",
|
|
88
|
+
powerTotalRated: "30 kW",
|
|
89
|
+
stockLevelStatus: "Available",
|
|
90
|
+
},
|
|
91
|
+
facets: sampleFacets,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const WithoutSpares: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
title: "Replacement Filter Element",
|
|
98
|
+
imageUrl: sampleImage,
|
|
99
|
+
code: "FE-001",
|
|
100
|
+
hit: {
|
|
101
|
+
objectID: "FE-001",
|
|
102
|
+
code: "FE-001",
|
|
103
|
+
name: "Replacement Filter Element",
|
|
104
|
+
title: "Replacement Filter Element",
|
|
105
|
+
image: sampleImage,
|
|
106
|
+
description: "<p>High-quality replacement filter for optimal performance.</p>",
|
|
107
|
+
price: "£35.00",
|
|
108
|
+
itemtype: "Filter",
|
|
109
|
+
stockLevelStatus: "In Stock",
|
|
110
|
+
spares: [],
|
|
111
|
+
},
|
|
112
|
+
facets: [
|
|
113
|
+
{ name: "itemtype", label: "Item Type" },
|
|
114
|
+
{ name: "stockLevelStatus", label: "Stock Level Status" },
|
|
115
|
+
{ name: "description", label: "Description", returnsHTML: true },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const NoImage: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
title: "Service Manual - Digital",
|
|
123
|
+
code: "SM-DIG-001",
|
|
124
|
+
hit: {
|
|
125
|
+
objectID: "SM-DIG-001",
|
|
126
|
+
code: "SM-DIG-001",
|
|
127
|
+
title: "Service Manual - Digital",
|
|
128
|
+
description: "<p>Comprehensive digital service manual for maintenance.</p>",
|
|
129
|
+
itemtype: "Documentation",
|
|
130
|
+
stockLevelStatus: "Available",
|
|
131
|
+
},
|
|
132
|
+
facets: [
|
|
133
|
+
{ name: "itemtype", label: "Item Type" },
|
|
134
|
+
{ name: "stockLevelStatus", label: "Availability" },
|
|
135
|
+
{ name: "description", label: "Description", returnsHTML: true },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ProductDetailsCard as ProductDetailsCardBase } from "@asantemedia-org/edwardsvacuum-design-system";
|
|
3
|
+
import { ProductCardHorizontalParts } from "../ProductCardHorizontal/ProductCardHorizontal";
|
|
4
|
+
import { ProductCardDetailsProps } from "../../types/cards";
|
|
5
|
+
import styles from "./ProductCardDetails.module.scss";
|
|
6
|
+
|
|
7
|
+
const SpareCardAdapter = (props: any) => {
|
|
8
|
+
const { spare, ...rest } = props;
|
|
9
|
+
return (
|
|
10
|
+
<ProductCardHorizontalParts
|
|
11
|
+
url={spare?.url || spare?.link || "#"}
|
|
12
|
+
title={spare?.name || spare?.title || ""}
|
|
13
|
+
description={spare?.description || ""}
|
|
14
|
+
price={spare?.priceValue || spare?.price || ""}
|
|
15
|
+
imageUrl={spare?.["img-product"] || spare?.image || ""}
|
|
16
|
+
button={{
|
|
17
|
+
label: rest.cta || "View Product",
|
|
18
|
+
onClick: () => window.open(spare?.url || spare?.link || "#", "_blank"),
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const ProductCardDetails: React.FC<ProductCardDetailsProps> = ({
|
|
25
|
+
id,
|
|
26
|
+
imageUrl,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
price,
|
|
30
|
+
code,
|
|
31
|
+
facets,
|
|
32
|
+
hit,
|
|
33
|
+
className = "",
|
|
34
|
+
ProductCardComponent,
|
|
35
|
+
}) => {
|
|
36
|
+
const hitData = hit || {
|
|
37
|
+
id,
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
price,
|
|
41
|
+
code,
|
|
42
|
+
image: imageUrl,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={styles.productDetails}>
|
|
47
|
+
<ProductDetailsCardBase
|
|
48
|
+
className={`${className}`}
|
|
49
|
+
title={title}
|
|
50
|
+
imageUrl={imageUrl}
|
|
51
|
+
imageAlt={title}
|
|
52
|
+
hit={hitData}
|
|
53
|
+
facets={facets}
|
|
54
|
+
usePlainClasses={true}
|
|
55
|
+
ProductCardComponent={ProductCardComponent || SpareCardAdapter}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default ProductCardDetails;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
@use "../../utils/styles/base.scss" as *;
|
|
2
|
+
|
|
3
|
+
.productCard {
|
|
4
|
+
display: block;
|
|
5
|
+
border: 1px solid transparent;
|
|
6
|
+
h3,
|
|
7
|
+
p {
|
|
8
|
+
margin: 0;
|
|
9
|
+
}
|
|
10
|
+
&:focus-visible {
|
|
11
|
+
outline: 1px solid $primary-color;
|
|
12
|
+
outline-offset: 1px;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.productWrapper {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: $spacing-sm;
|
|
20
|
+
padding: calc($spacing-md * 0.5) !important;
|
|
21
|
+
background-color: white;
|
|
22
|
+
box-shadow: 2.7px 2.7px 5px 0 rgba(0, 0, 0, 0.1);
|
|
23
|
+
|
|
24
|
+
&.withImage {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: 75px repeat(2, minmax(80px, 1fr));
|
|
27
|
+
grid-template-rows: repeat(3, auto);
|
|
28
|
+
grid-template-areas:
|
|
29
|
+
"image description description"
|
|
30
|
+
"image description description"
|
|
31
|
+
"image cta cta";
|
|
32
|
+
row-gap: calc($spacing-xs * 0.5);
|
|
33
|
+
.productTitle {
|
|
34
|
+
font-size: clamp(12px, 1.25vw, 14px);
|
|
35
|
+
}
|
|
36
|
+
.productDescription {
|
|
37
|
+
font-size: clamp(12px, 1.25vw, 14px);
|
|
38
|
+
}
|
|
39
|
+
.productPrice {
|
|
40
|
+
font-size: clamp(12px, 1.25vw, 14px);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.productImage {
|
|
46
|
+
grid-area: image;
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.productTitle,
|
|
55
|
+
h3.productTitle {
|
|
56
|
+
margin: 0 0 0.25rem 0;
|
|
57
|
+
font-size: clamp(12px, 1.25vw, 14px);
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
color: $text-primary;
|
|
60
|
+
line-height: 1.4;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.productDescription {
|
|
64
|
+
margin: 0 0 0.5rem 0;
|
|
65
|
+
font-size: clamp(14px, 1.25vw, 1.5rem);
|
|
66
|
+
color: $text-secondary;
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
display: -webkit-box;
|
|
69
|
+
-webkit-line-clamp: 2;
|
|
70
|
+
-webkit-box-orient: vertical;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.productPrice {
|
|
75
|
+
margin: 0;
|
|
76
|
+
font-size: clamp(14px, 1.25vw, 1.5rem);
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
color: $text-primary;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.productTitleDescriptionWrapper {
|
|
82
|
+
grid-area: description;
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: 0.25rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.productPriceCtaWrapper {
|
|
89
|
+
grid-area: cta;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
}
|