@4alldigital/foundation-ui--core 3.3.0 → 3.4.1
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/package.json +2 -2
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Form/Form.tsx +1 -1
- package/src/components/ProductCard/ProductCard.stories.tsx +90 -0
- package/src/components/ProductCard/ProductCard.tsx +110 -0
- package/src/components/ProductCard/ProductCard.types.ts +21 -0
- package/src/components/ProductCard/index.ts +3 -0
- package/src/components/ProductDetail/ProductDetail.stories.tsx +97 -0
- package/src/components/ProductDetail/ProductDetail.tsx +251 -0
- package/src/components/ProductDetail/ProductDetail.types.ts +16 -0
- package/src/components/ProductDetail/index.ts +2 -0
- package/src/components/ShadcnButton/ShadcnButton.stories.tsx +152 -0
- package/src/components/ShadcnCarousel/ShadcnCarousel.stories.tsx +179 -0
- package/src/components/VariantSelector/VariantSelector.stories.tsx +107 -0
- package/src/components/VariantSelector/VariantSelector.tsx +118 -0
- package/src/components/VariantSelector/VariantSelector.types.ts +15 -0
- package/src/components/VariantSelector/index.ts +2 -0
- package/src/components/index.ts +17 -0
- package/src/context/Amplify/index.tsx +1 -0
- package/src/context/Theme/index.tsx +0 -2
- package/src/forms/LoginForm/LoginForm.tsx +1 -3
- package/src/templates/ProductDetailScreen/ProductDetailScreen.stories.tsx +108 -0
- package/src/templates/ProductDetailScreen/ProductDetailScreen.tsx +35 -0
- package/src/templates/ProductDetailScreen/ProductDetailScreen.types.ts +11 -0
- package/src/templates/ProductDetailScreen/index.ts +1 -0
- package/src/templates/ProductListScreen/ProductListScreen.stories.tsx +121 -0
- package/src/templates/ProductListScreen/ProductListScreen.tsx +59 -0
- package/src/templates/ProductListScreen/ProductListScreen.types.ts +10 -0
- package/src/templates/ProductListScreen/index.ts +1 -0
- package/src/templates/index.ts +4 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import ProductDetailScreen from './ProductDetailScreen';
|
|
3
|
+
import { Product } from '../../components/ProductCard';
|
|
4
|
+
import { ProductDetailState } from '../../components/ProductDetail';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ProductDetailScreen> = {
|
|
7
|
+
title: 'Templates/ProductDetailScreen',
|
|
8
|
+
component: ProductDetailScreen,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof ProductDetailScreen>;
|
|
16
|
+
|
|
17
|
+
const sampleProduct: Product = {
|
|
18
|
+
id: 'prod_1',
|
|
19
|
+
name: 'NRG Performance Leggings',
|
|
20
|
+
description: 'High-performance leggings designed for intense workouts. Made with moisture-wicking fabric that keeps you dry and comfortable during your toughest training sessions.',
|
|
21
|
+
images: [
|
|
22
|
+
'https://picsum.photos/800/800?random=1',
|
|
23
|
+
'https://picsum.photos/800/800?random=2',
|
|
24
|
+
'https://picsum.photos/800/800?random=3',
|
|
25
|
+
],
|
|
26
|
+
price: 4500,
|
|
27
|
+
currency: 'gbp',
|
|
28
|
+
metadata: {
|
|
29
|
+
partnerId: 'lululemon',
|
|
30
|
+
sizes: ['XS', 'S', 'M', 'L', 'XL'],
|
|
31
|
+
colors: ['Black', 'Navy', 'Burgundy'],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
product: sampleProduct,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
isPurchasing: false,
|
|
40
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Loading: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
product: sampleProduct,
|
|
47
|
+
isLoading: true,
|
|
48
|
+
isPurchasing: false,
|
|
49
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Purchasing: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
product: sampleProduct,
|
|
56
|
+
isLoading: false,
|
|
57
|
+
isPurchasing: true,
|
|
58
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const SingleImage: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
product: {
|
|
65
|
+
...sampleProduct,
|
|
66
|
+
images: ['https://picsum.photos/800/800?random=1'],
|
|
67
|
+
},
|
|
68
|
+
isLoading: false,
|
|
69
|
+
isPurchasing: false,
|
|
70
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const NoImages: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
product: {
|
|
77
|
+
...sampleProduct,
|
|
78
|
+
images: [],
|
|
79
|
+
},
|
|
80
|
+
isLoading: false,
|
|
81
|
+
isPurchasing: false,
|
|
82
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const NoVariants: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
product: {
|
|
89
|
+
...sampleProduct,
|
|
90
|
+
metadata: {
|
|
91
|
+
partnerId: 'lululemon',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
isLoading: false,
|
|
95
|
+
isPurchasing: false,
|
|
96
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Unauthenticated: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
product: sampleProduct,
|
|
103
|
+
isLoading: false,
|
|
104
|
+
isPurchasing: false,
|
|
105
|
+
isAuthenticated: false,
|
|
106
|
+
onPurchase: (state: ProductDetailState) => console.log('Purchase:', state),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Screen from '../../components/Screen';
|
|
3
|
+
import { ProductDetail } from '../../components/ProductDetail';
|
|
4
|
+
import { Props } from './ProductDetailScreen.types';
|
|
5
|
+
|
|
6
|
+
const ProductDetailScreen = ({ testID, product, isLoading, isPurchasing, isAuthenticated = true, onPurchase }: Props) => {
|
|
7
|
+
return (
|
|
8
|
+
<Screen data-testid={testID || 'ProductDetailScreen'}>
|
|
9
|
+
<div className="container mx-auto px-4 py-16">
|
|
10
|
+
{isLoading ? (
|
|
11
|
+
<div className="grid gap-8 lg:grid-cols-2">
|
|
12
|
+
<div className="aspect-square animate-pulse rounded-lg bg-muted"></div>
|
|
13
|
+
<div className="space-y-4">
|
|
14
|
+
<div className="h-10 w-3/4 animate-pulse rounded bg-muted"></div>
|
|
15
|
+
<div className="h-8 w-1/4 animate-pulse rounded bg-muted"></div>
|
|
16
|
+
<div className="h-20 w-full animate-pulse rounded bg-muted"></div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
) : (
|
|
20
|
+
<ProductDetail
|
|
21
|
+
product={product}
|
|
22
|
+
onPurchase={onPurchase}
|
|
23
|
+
isLoading={isPurchasing}
|
|
24
|
+
isAuthenticated={isAuthenticated}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
</Screen>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
ProductDetailScreen.displayName = 'ProductDetailScreen';
|
|
33
|
+
|
|
34
|
+
export default ProductDetailScreen;
|
|
35
|
+
export type { Props };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Product } from '../../components/ProductCard';
|
|
2
|
+
import { ProductDetailState } from '../../components/ProductDetail';
|
|
3
|
+
|
|
4
|
+
export interface Props {
|
|
5
|
+
testID?: string;
|
|
6
|
+
product: Product;
|
|
7
|
+
isLoading?: boolean;
|
|
8
|
+
isPurchasing?: boolean;
|
|
9
|
+
isAuthenticated?: boolean;
|
|
10
|
+
onPurchase: (state: ProductDetailState) => void;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ProductDetailScreen';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import ProductListScreen from './ProductListScreen';
|
|
3
|
+
import { Product } from '../../components/ProductCard';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ProductListScreen> = {
|
|
6
|
+
title: 'Templates/ProductListScreen',
|
|
7
|
+
component: ProductListScreen,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof ProductListScreen>;
|
|
15
|
+
|
|
16
|
+
const sampleProducts: Product[] = [
|
|
17
|
+
{
|
|
18
|
+
id: 'prod_1',
|
|
19
|
+
name: 'Performance Leggings',
|
|
20
|
+
description: 'High-performance leggings designed for intense workouts',
|
|
21
|
+
images: ['https://picsum.photos/400/400?random=1'],
|
|
22
|
+
price: 4500,
|
|
23
|
+
currency: 'gbp',
|
|
24
|
+
metadata: {
|
|
25
|
+
partnerId: 'partner1',
|
|
26
|
+
sizes: ['XS', 'S', 'M', 'L', 'XL'],
|
|
27
|
+
colors: ['Black', 'Navy', 'Burgundy'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'prod_2',
|
|
32
|
+
name: 'Athletic Tank Top',
|
|
33
|
+
description: 'Breathable tank top for all-day comfort',
|
|
34
|
+
images: ['https://picsum.photos/400/400?random=2'],
|
|
35
|
+
price: 2500,
|
|
36
|
+
currency: 'gbp',
|
|
37
|
+
metadata: {
|
|
38
|
+
partnerId: 'partner1',
|
|
39
|
+
sizes: ['XS', 'S', 'M', 'L', 'XL'],
|
|
40
|
+
colors: ['White', 'Black', 'Grey'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'prod_3',
|
|
45
|
+
name: 'Training Shorts',
|
|
46
|
+
description: 'Lightweight shorts perfect for running',
|
|
47
|
+
images: ['https://picsum.photos/400/400?random=3'],
|
|
48
|
+
price: 3200,
|
|
49
|
+
currency: 'gbp',
|
|
50
|
+
metadata: {
|
|
51
|
+
partnerId: 'partner1',
|
|
52
|
+
sizes: ['S', 'M', 'L', 'XL'],
|
|
53
|
+
colors: ['Black', 'Navy'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'prod_4',
|
|
58
|
+
name: 'Sports Bra',
|
|
59
|
+
description: 'High-support sports bra for intense workouts',
|
|
60
|
+
images: ['https://picsum.photos/400/400?random=4'],
|
|
61
|
+
price: 3800,
|
|
62
|
+
currency: 'gbp',
|
|
63
|
+
metadata: {
|
|
64
|
+
partnerId: 'partner2',
|
|
65
|
+
sizes: ['XS', 'S', 'M', 'L'],
|
|
66
|
+
colors: ['Black', 'White', 'Pink'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export const Default: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
title: 'Shop Partnerships',
|
|
74
|
+
products: sampleProducts,
|
|
75
|
+
isLoading: false,
|
|
76
|
+
onProductClick: (product) => console.log('Product clicked:', product),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const Loading: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
title: 'Shop Partnerships',
|
|
83
|
+
products: [],
|
|
84
|
+
isLoading: true,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Empty: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
title: 'Shop Partnerships',
|
|
91
|
+
products: [],
|
|
92
|
+
isLoading: false,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const Error: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
title: 'Shop Partnerships',
|
|
99
|
+
products: [],
|
|
100
|
+
isLoading: false,
|
|
101
|
+
error: 'Failed to load products. Please try again later.',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const SingleProduct: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
title: 'Shop Partnerships',
|
|
108
|
+
products: [sampleProducts[0]],
|
|
109
|
+
isLoading: false,
|
|
110
|
+
onProductClick: (product) => console.log('Product clicked:', product),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const ManyProducts: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
title: 'Shop Partnerships',
|
|
117
|
+
products: [...sampleProducts, ...sampleProducts, ...sampleProducts],
|
|
118
|
+
isLoading: false,
|
|
119
|
+
onProductClick: (product) => console.log('Product clicked:', product),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Screen from '../../components/Screen';
|
|
3
|
+
import DisplayHeading from '../../components/DisplayHeading';
|
|
4
|
+
import { ProductCard } from '../../components/ProductCard';
|
|
5
|
+
import { Props } from './ProductListScreen.types';
|
|
6
|
+
|
|
7
|
+
const ProductListScreen = ({ testID, title, products, isLoading, error, onProductClick }: Props) => {
|
|
8
|
+
return (
|
|
9
|
+
<Screen data-testid={testID || 'ProductListScreen'}>
|
|
10
|
+
{title && <DisplayHeading text={title} />}
|
|
11
|
+
|
|
12
|
+
<div className="container mx-auto px-4 py-8">
|
|
13
|
+
{error && (
|
|
14
|
+
<div className="rounded-lg bg-destructive/10 p-8 text-center">
|
|
15
|
+
<h2 className="mb-2 text-2xl font-bold text-destructive">Unable to Load Products</h2>
|
|
16
|
+
<p className="text-muted-foreground">{error}</p>
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
|
|
20
|
+
{isLoading && (
|
|
21
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
22
|
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
23
|
+
<div key={i} className="space-y-4">
|
|
24
|
+
<div className="aspect-square animate-pulse rounded-lg bg-muted"></div>
|
|
25
|
+
<div className="space-y-2">
|
|
26
|
+
<div className="h-6 w-3/4 animate-pulse rounded bg-muted"></div>
|
|
27
|
+
<div className="h-5 w-1/2 animate-pulse rounded bg-muted"></div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
{!isLoading && !error && products.length === 0 && (
|
|
35
|
+
<div className="rounded-lg border border-border bg-card p-12 text-center">
|
|
36
|
+
<p className="text-lg text-muted-foreground">No products available at this time.</p>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{!isLoading && !error && products.length > 0 && (
|
|
41
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
42
|
+
{products.map((product) => (
|
|
43
|
+
<ProductCard
|
|
44
|
+
key={product.id}
|
|
45
|
+
product={product}
|
|
46
|
+
onClick={onProductClick}
|
|
47
|
+
/>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</Screen>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
ProductListScreen.displayName = 'ProductListScreen';
|
|
57
|
+
|
|
58
|
+
export default ProductListScreen;
|
|
59
|
+
export type { Props };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ProductListScreen';
|
package/src/templates/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export { default as MenuScreen } from './MenuScreen';
|
|
|
13
13
|
export { default as NotFoundScreen } from './NotFoundScreen';
|
|
14
14
|
export { default as PasswordResetScreen } from './PasswordResetScreen';
|
|
15
15
|
export { default as PasswordResetAuthScreen } from './PasswordResetAuthScreen';
|
|
16
|
+
export { default as ProductDetailScreen } from './ProductDetailScreen';
|
|
17
|
+
export { default as ProductListScreen } from './ProductListScreen';
|
|
16
18
|
export { default as ProfileScreen } from './ProfileScreen';
|
|
17
19
|
export { default as ScheduleScreen } from './ScheduleScreen';
|
|
18
20
|
export { default as SubscriptionScreen } from './SubscriptionScreen';
|
|
@@ -34,6 +36,8 @@ export type { Props as MenuScreenProps } from './MenuScreen/MenuScreen.types';
|
|
|
34
36
|
export type { Props as NotFoundScreenProps } from './NotFoundScreen/NotFoundScreen.types';
|
|
35
37
|
export type { Props as PasswordResetScreenProps } from './PasswordResetScreen/PasswordResetScreen.types';
|
|
36
38
|
export type { Props as PasswordResetAuthScreenProps } from './PasswordResetAuthScreen/PasswordResetAuthScreen.types';
|
|
39
|
+
export type { Props as ProductDetailScreenProps } from './ProductDetailScreen/ProductDetailScreen.types';
|
|
40
|
+
export type { Props as ProductListScreenProps } from './ProductListScreen/ProductListScreen.types';
|
|
37
41
|
export type { Props as ProfileScreenProps } from './ProfileScreen/ProfileScreen.types';
|
|
38
42
|
export type { Props as ScheduleScreenProps } from './ScheduleScreen/ScheduleScreen.types';
|
|
39
43
|
export type { Props as SubscriptionScreenProps } from './SubscriptionScreen/SubscriptionScreen.types';
|