@shopify/shop-minis-react 0.0.24 → 0.0.26
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/dist/_virtual/index10.js +2 -2
- package/dist/_virtual/index5.js +2 -3
- package/dist/_virtual/index5.js.map +1 -1
- package/dist/_virtual/index6.js +3 -2
- package/dist/_virtual/index6.js.map +1 -1
- package/dist/_virtual/index7.js +2 -2
- package/dist/_virtual/index9.js +2 -2
- package/dist/components/atoms/product-variant-price.js +61 -0
- package/dist/components/atoms/product-variant-price.js.map +1 -0
- package/dist/components/commerce/product-card.js +120 -153
- package/dist/components/commerce/product-card.js.map +1 -1
- package/dist/components/commerce/product-link.js +12 -16
- package/dist/components/commerce/product-link.js.map +1 -1
- package/dist/components/content/content-monitor.js +17 -0
- package/dist/components/content/content-monitor.js.map +1 -0
- package/dist/components/content/content-wrapper.js +17 -0
- package/dist/components/content/content-wrapper.js.map +1 -0
- package/dist/hooks/content/useContent.js +24 -0
- package/dist/hooks/content/useContent.js.map +1 -0
- package/dist/hooks/content/useCreateImageContent.js +40 -0
- package/dist/hooks/content/useCreateImageContent.js.map +1 -0
- package/dist/hooks/storage/useImageUpload.js +49 -40
- package/dist/hooks/storage/useImageUpload.js.map +1 -1
- package/dist/index.js +225 -217
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +82 -50
- package/dist/mocks.js.map +1 -1
- package/dist/shop-minis-platform/src/types/content.js +5 -0
- package/dist/shop-minis-platform/src/types/content.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js +1 -1
- package/dist/utils/imageToDataUri.js +10 -0
- package/dist/utils/imageToDataUri.js.map +1 -0
- package/package.json +5 -4
- package/src/components/atoms/product-variant-price.tsx +74 -0
- package/src/components/commerce/product-card.tsx +7 -56
- package/src/components/commerce/product-link.tsx +0 -2
- package/src/components/content/content-monitor.tsx +23 -0
- package/src/components/content/content-wrapper.tsx +56 -0
- package/src/components/index.ts +2 -0
- package/src/hooks/content/useContent.ts +50 -0
- package/src/hooks/content/useCreateImageContent.ts +80 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/storage/useImageUpload.ts +27 -11
- package/src/mocks.ts +32 -0
- package/src/stories/ProductVariantPrice.stories.tsx +73 -0
- package/src/stories/Toaster.stories.tsx +2 -2
- package/src/utils/imageToDataUri.ts +8 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// import {useShopActions} from '../../internal/useShopActions'
|
|
2
|
+
import {Touchable} from '../atoms/touchable'
|
|
3
|
+
|
|
4
|
+
export function ContentMonitor({
|
|
5
|
+
// publicId,
|
|
6
|
+
children,
|
|
7
|
+
}: {
|
|
8
|
+
publicId: string
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
}) {
|
|
11
|
+
// const {showFeedbackSheet} = useShopActions()
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Touchable
|
|
15
|
+
// TODO: Add long press support to Touchable
|
|
16
|
+
// onLongPress={() => {
|
|
17
|
+
// showFeedbackSheet({publicId})
|
|
18
|
+
// }}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</Touchable>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {useContent} from '../../hooks/content/useContent'
|
|
2
|
+
import {Content} from '../../types'
|
|
3
|
+
|
|
4
|
+
import {ContentMonitor} from './content-monitor'
|
|
5
|
+
|
|
6
|
+
interface BaseContentWrapperProps {
|
|
7
|
+
children: ({
|
|
8
|
+
content,
|
|
9
|
+
loading,
|
|
10
|
+
}: {
|
|
11
|
+
content?: Content
|
|
12
|
+
loading: boolean
|
|
13
|
+
}) => JSX.Element | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PublicIdContentWrapperProps extends BaseContentWrapperProps {
|
|
17
|
+
publicId: string
|
|
18
|
+
externalId?: never
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ExternalIdContentWrapperProps extends BaseContentWrapperProps {
|
|
22
|
+
externalId: string
|
|
23
|
+
publicId?: never
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ContentWrapperProps =
|
|
27
|
+
| PublicIdContentWrapperProps
|
|
28
|
+
| ExternalIdContentWrapperProps
|
|
29
|
+
|
|
30
|
+
// It's too messy in the docs to show the complete types here so we show a simplified version
|
|
31
|
+
export interface ContentWrapperPropsForDocs extends BaseContentWrapperProps {
|
|
32
|
+
publicId?: string
|
|
33
|
+
externalId?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ContentWrapper({
|
|
37
|
+
publicId,
|
|
38
|
+
externalId,
|
|
39
|
+
children,
|
|
40
|
+
}: ContentWrapperProps) {
|
|
41
|
+
const {content, loading} = useContent({
|
|
42
|
+
identifiers: [{publicId, externalId}],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const contentItem = content?.[0]
|
|
46
|
+
|
|
47
|
+
if (loading || !contentItem) {
|
|
48
|
+
return children({loading})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ContentMonitor publicId={contentItem.publicId}>
|
|
53
|
+
{children({content: contentItem, loading})}
|
|
54
|
+
</ContentMonitor>
|
|
55
|
+
)
|
|
56
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export * from './commerce/product-card-skeleton'
|
|
|
7
7
|
export * from './commerce/merchant-card-skeleton'
|
|
8
8
|
export * from './commerce/quantity-selector'
|
|
9
9
|
|
|
10
|
+
export * from './content/content-wrapper'
|
|
11
|
+
|
|
10
12
|
export * from './navigation/transition-container'
|
|
11
13
|
export * from './navigation/transition-link'
|
|
12
14
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {useMemo} from 'react'
|
|
2
|
+
|
|
3
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
4
|
+
import {useShopActionsDataFetching} from '../../internal/useShopActionsDataFetching'
|
|
5
|
+
import {
|
|
6
|
+
Content,
|
|
7
|
+
ContentIdentifierInput,
|
|
8
|
+
DataHookOptionsBase,
|
|
9
|
+
DataHookReturnsBase,
|
|
10
|
+
} from '../../types'
|
|
11
|
+
|
|
12
|
+
export interface UseContentParams extends DataHookOptionsBase {
|
|
13
|
+
/**
|
|
14
|
+
* The identifiers of the content to fetch.
|
|
15
|
+
*/
|
|
16
|
+
identifiers: ContentIdentifierInput | ContentIdentifierInput[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UseContentReturns extends DataHookReturnsBase {
|
|
20
|
+
/**
|
|
21
|
+
* The content returned from the query.
|
|
22
|
+
*/
|
|
23
|
+
content: Content[] | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const useContent = (params: UseContentParams): UseContentReturns => {
|
|
27
|
+
const {getContent} = useShopActions()
|
|
28
|
+
const {identifiers, skip = false, ...restParams} = params
|
|
29
|
+
|
|
30
|
+
const {data, ...rest} = useShopActionsDataFetching(
|
|
31
|
+
getContent,
|
|
32
|
+
{
|
|
33
|
+
identifiers,
|
|
34
|
+
...restParams,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
skip,
|
|
38
|
+
hook: 'useContent',
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const content = useMemo(() => {
|
|
43
|
+
return data ?? null
|
|
44
|
+
}, [data])
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...rest,
|
|
48
|
+
content,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {useCallback, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ContentVisibility,
|
|
5
|
+
Content,
|
|
6
|
+
ContentCreateUserErrors,
|
|
7
|
+
} from '@shopify/shop-minis-platform'
|
|
8
|
+
|
|
9
|
+
import {useHandleAction} from '../../internal/useHandleAction'
|
|
10
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
11
|
+
import {fileToDataUri} from '../../utils'
|
|
12
|
+
import {useImageUpload} from '../storage/useImageUpload'
|
|
13
|
+
|
|
14
|
+
interface CreateImageContentParams {
|
|
15
|
+
image: File
|
|
16
|
+
contentTitle: string
|
|
17
|
+
visibility?: ContentVisibility[] | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseCreateImageContentReturns {
|
|
21
|
+
/**
|
|
22
|
+
* Upload an image and create content.
|
|
23
|
+
*/
|
|
24
|
+
createImageContent: (
|
|
25
|
+
params: CreateImageContentParams
|
|
26
|
+
) => Promise<{data: Content; userErrors?: ContentCreateUserErrors[]}>
|
|
27
|
+
/**
|
|
28
|
+
* Whether the content is being created.
|
|
29
|
+
*/
|
|
30
|
+
loading: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const useCreateImageContent = (): UseCreateImageContentReturns => {
|
|
34
|
+
const {createContent} = useShopActions()
|
|
35
|
+
const {uploadImage} = useImageUpload()
|
|
36
|
+
const [loading, setLoading] = useState(false)
|
|
37
|
+
|
|
38
|
+
const createImageContent = useCallback(
|
|
39
|
+
async (params: CreateImageContentParams) => {
|
|
40
|
+
setLoading(true)
|
|
41
|
+
|
|
42
|
+
const {image, contentTitle, visibility} = params
|
|
43
|
+
|
|
44
|
+
if (!image.type) {
|
|
45
|
+
throw new Error('Unable to determine file type')
|
|
46
|
+
}
|
|
47
|
+
if (!image.type.startsWith('image/')) {
|
|
48
|
+
throw new Error('Invalid file type: must be an image')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [uploadImageResult] = await uploadImage([
|
|
52
|
+
{
|
|
53
|
+
mimeType: image.type,
|
|
54
|
+
uri: await fileToDataUri(image),
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
const uploadImageUrl = uploadImageResult.imageUrl
|
|
58
|
+
|
|
59
|
+
if (!uploadImageUrl) {
|
|
60
|
+
throw new Error('Image upload failed')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const createContentResult = await createContent({
|
|
64
|
+
title: contentTitle,
|
|
65
|
+
imageUrl: uploadImageUrl,
|
|
66
|
+
visibility,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
setLoading(false)
|
|
70
|
+
|
|
71
|
+
return createContentResult
|
|
72
|
+
},
|
|
73
|
+
[createContent, uploadImage]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
createImageContent: useHandleAction(createImageContent),
|
|
78
|
+
loading,
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface UploadImageParams {
|
|
|
12
12
|
/**
|
|
13
13
|
* The size of the image in bytes.
|
|
14
14
|
*/
|
|
15
|
-
fileSize
|
|
15
|
+
fileSize?: number
|
|
16
16
|
/**
|
|
17
17
|
* The URI of the image to upload.
|
|
18
18
|
*/
|
|
@@ -41,16 +41,29 @@ interface UseImageUploadReturns {
|
|
|
41
41
|
uploadImage: (params: UploadImageParams[]) => Promise<UploadedImage[]>
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Fetch file data and detect file sizes if not provided
|
|
45
|
+
// Works with file://, data:, and http(s):// URIs
|
|
46
|
+
const processFileData = async (image: UploadImageParams) => {
|
|
47
|
+
const response = await fetch(image.uri)
|
|
48
|
+
const blob = await response.blob()
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...image,
|
|
52
|
+
fileSize: image.fileSize ?? blob.size,
|
|
53
|
+
fileBlob: blob,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
const uploadFileToGCS = async (
|
|
45
|
-
image: UploadImageParams,
|
|
58
|
+
image: UploadImageParams & {fileSize: number; fileBlob: Blob},
|
|
46
59
|
target: UploadTarget
|
|
47
60
|
) => {
|
|
48
61
|
const formData = new FormData()
|
|
49
62
|
target.parameters.forEach(({name, value}: {name: string; value: string}) => {
|
|
50
63
|
formData.append(name, value)
|
|
51
64
|
})
|
|
52
|
-
|
|
53
|
-
formData.append('file',
|
|
65
|
+
|
|
66
|
+
formData.append('file', image.fileBlob)
|
|
54
67
|
|
|
55
68
|
const uploadResponse = await fetch(target.url, {
|
|
56
69
|
method: 'POST',
|
|
@@ -76,11 +89,16 @@ export const useImageUpload = (): UseImageUploadReturns => {
|
|
|
76
89
|
throw new Error('Multiple image upload is not supported yet')
|
|
77
90
|
}
|
|
78
91
|
|
|
92
|
+
const imageParams = params[0]
|
|
93
|
+
const processedImageParams = await processFileData(imageParams)
|
|
94
|
+
|
|
79
95
|
const links = await createImageUploadLink({
|
|
80
|
-
input:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
input: [
|
|
97
|
+
{
|
|
98
|
+
mimeType: processedImageParams.mimeType,
|
|
99
|
+
fileSize: processedImageParams.fileSize,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
84
102
|
})
|
|
85
103
|
|
|
86
104
|
if (!links.ok) {
|
|
@@ -88,9 +106,8 @@ export const useImageUpload = (): UseImageUploadReturns => {
|
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
// Upload single file to GCS
|
|
91
|
-
// TODO: Upload multiple files to GCS
|
|
92
109
|
const {error: uploadError} = await uploadFileToGCS(
|
|
93
|
-
|
|
110
|
+
processedImageParams,
|
|
94
111
|
links?.data?.targets?.[0]!
|
|
95
112
|
)
|
|
96
113
|
|
|
@@ -110,7 +127,6 @@ export const useImageUpload = (): UseImageUploadReturns => {
|
|
|
110
127
|
throw new Error(result.error.message)
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
// TODO: Add support for multiple files
|
|
114
130
|
if (result.data?.files?.[0]?.fileStatus === 'READY') {
|
|
115
131
|
return [
|
|
116
132
|
{
|
package/src/mocks.ts
CHANGED
|
@@ -405,6 +405,38 @@ function makeMockActions(): ShopActions {
|
|
|
405
405
|
pageInfo: createPagination(),
|
|
406
406
|
},
|
|
407
407
|
previewProductInAr: undefined,
|
|
408
|
+
createContent: {
|
|
409
|
+
data: {
|
|
410
|
+
publicId: 'content-123',
|
|
411
|
+
externalId: null,
|
|
412
|
+
image: {
|
|
413
|
+
id: 'img-123',
|
|
414
|
+
url: 'https://example.com/content-image.jpg',
|
|
415
|
+
width: 800,
|
|
416
|
+
height: 600,
|
|
417
|
+
},
|
|
418
|
+
title: 'Mock Content',
|
|
419
|
+
description: 'This is a mock content item',
|
|
420
|
+
visibility: ['DISCOVERABLE'],
|
|
421
|
+
shareableUrl: 'https://example.com/content/123',
|
|
422
|
+
products: null,
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
getContent: {
|
|
426
|
+
data: [
|
|
427
|
+
{
|
|
428
|
+
publicId: 'content-123',
|
|
429
|
+
image: {
|
|
430
|
+
id: 'img-123',
|
|
431
|
+
url: 'https://example.com/content-image.jpg',
|
|
432
|
+
width: 800,
|
|
433
|
+
height: 600,
|
|
434
|
+
},
|
|
435
|
+
title: 'Mock Content',
|
|
436
|
+
visibility: ['DISCOVERABLE'],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
},
|
|
408
440
|
} as const
|
|
409
441
|
|
|
410
442
|
const mock: Partial<ShopActions> = {}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {ProductVariantPrice} from '../components/atoms/product-variant-price'
|
|
2
|
+
|
|
3
|
+
import type {Meta, StoryObj} from '@storybook/react-vite'
|
|
4
|
+
|
|
5
|
+
type ProductVariantPriceProps = React.ComponentProps<typeof ProductVariantPrice>
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Atoms/ProductVariantPrice',
|
|
9
|
+
component: ProductVariantPrice,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
amount: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
},
|
|
17
|
+
currencyCode: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['USD', 'CAD', 'EUR', 'GBP', 'JPY'],
|
|
20
|
+
},
|
|
21
|
+
compareAtPriceAmount: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
},
|
|
24
|
+
compareAtPriceCurrencyCode: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
options: ['USD', 'CAD', 'EUR', 'GBP', 'JPY'],
|
|
27
|
+
},
|
|
28
|
+
className: {
|
|
29
|
+
control: 'text',
|
|
30
|
+
},
|
|
31
|
+
currentPriceClassName: {
|
|
32
|
+
control: 'text',
|
|
33
|
+
},
|
|
34
|
+
originalPriceClassName: {
|
|
35
|
+
control: 'text',
|
|
36
|
+
},
|
|
37
|
+
containerClassName: {
|
|
38
|
+
control: 'text',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
tags: ['autodocs'],
|
|
42
|
+
} satisfies Meta<ProductVariantPriceProps>
|
|
43
|
+
|
|
44
|
+
export default meta
|
|
45
|
+
type Story = StoryObj<typeof meta>
|
|
46
|
+
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
amount: '29.99',
|
|
50
|
+
currencyCode: 'USD',
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const WithDiscount: Story = {
|
|
55
|
+
name: 'With Discount',
|
|
56
|
+
args: {
|
|
57
|
+
amount: '24.99',
|
|
58
|
+
currencyCode: 'USD',
|
|
59
|
+
compareAtPriceAmount: '39.99',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const CustomStyling: Story = {
|
|
64
|
+
name: 'Custom Styling',
|
|
65
|
+
args: {
|
|
66
|
+
amount: '89.99',
|
|
67
|
+
currencyCode: 'USD',
|
|
68
|
+
compareAtPriceAmount: '119.99',
|
|
69
|
+
currentPriceClassName: 'text-2xl font-bold text-green-600',
|
|
70
|
+
originalPriceClassName: 'text-lg text-red-500 line-through',
|
|
71
|
+
containerClassName: 'gap-3 p-4 bg-gray-50 rounded-lg',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
@@ -26,7 +26,7 @@ type Story = StoryObj<typeof meta>
|
|
|
26
26
|
|
|
27
27
|
export const SuccessToast: Story = {
|
|
28
28
|
decorators: [
|
|
29
|
-
|
|
29
|
+
() => (
|
|
30
30
|
<Button onClick={() => toast.success('Success toast!')}>
|
|
31
31
|
Show success Toast
|
|
32
32
|
</Button>
|
|
@@ -37,7 +37,7 @@ export const SuccessToast: Story = {
|
|
|
37
37
|
|
|
38
38
|
export const ErrorToast: Story = {
|
|
39
39
|
decorators: [
|
|
40
|
-
|
|
40
|
+
() => (
|
|
41
41
|
<Button onClick={() => toast.error('Error toast!')}>
|
|
42
42
|
Show error Toast
|
|
43
43
|
</Button>
|
package/src/utils/index.ts
CHANGED