@shopify/shop-minis-react 0.0.25 → 0.0.27
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/index4.js +2 -3
- package/dist/_virtual/index4.js.map +1 -1
- package/dist/_virtual/index7.js +3 -2
- package/dist/_virtual/index7.js.map +1 -1
- 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-skeleton.js +30 -0
- package/dist/components/commerce/product-link-skeleton.js.map +1 -0
- package/dist/components/commerce/product-link.js +73 -77
- package/dist/components/commerce/product-link.js.map +1 -1
- package/dist/components/commerce/search.js +144 -0
- package/dist/components/commerce/search.js.map +1 -0
- 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/components/ui/input.js +3 -3
- package/dist/components/ui/input.js.map +1 -1
- package/dist/hooks/content/useContent.js +24 -0
- package/dist/hooks/content/useContent.js.map +1 -0
- package/dist/hooks/content/useCreateImageContent.js +21 -18
- package/dist/hooks/content/useCreateImageContent.js.map +1 -1
- package/dist/hooks/product/useProductSearch.js +24 -23
- package/dist/hooks/product/useProductSearch.js.map +1 -1
- package/dist/index.js +230 -221
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +21 -6
- 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/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/search.js +16 -0
- package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/search.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.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/shop-minis-react.css +1 -1
- 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-skeleton.tsx +30 -0
- package/src/components/commerce/product-link.tsx +8 -7
- package/src/components/commerce/search.tsx +264 -0
- package/src/components/content/content-monitor.tsx +23 -0
- package/src/components/content/content-wrapper.tsx +56 -0
- package/src/components/index.ts +3 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/hooks/content/useContent.ts +50 -0
- package/src/hooks/content/useCreateImageContent.ts +20 -5
- package/src/hooks/product/useProductSearch.ts +10 -1
- package/src/mocks.ts +15 -0
- package/src/stories/ProductVariantPrice.stories.tsx +73 -0
- package/src/stories/Toaster.stories.tsx +2 -2
- package/src/styles/utilities.css +9 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import {cn} from '../../lib/utils'
|
|
4
|
+
import {Skeleton} from '../ui/skeleton'
|
|
5
|
+
|
|
6
|
+
function ProductLinkSkeleton({
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<'div'>) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className={cn(
|
|
13
|
+
'relative w-full shadow-sm rounded-lg p-4 items-center',
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<div className="flex flex-row items-center justify-center w-full">
|
|
19
|
+
<Skeleton className="aspect-square w-15 h-15" />
|
|
20
|
+
<div className="flex flex-col justify-center items-start ml-2 w-full pt-2">
|
|
21
|
+
<Skeleton className="mb-3 h-3 w-3/4" />
|
|
22
|
+
<Skeleton className="mb-2 h-3 w-1/4" />
|
|
23
|
+
<Skeleton className="mb-2 h-3 w-1/3" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {ProductLinkSkeleton}
|
|
@@ -222,10 +222,11 @@ function ProductLinkActions({
|
|
|
222
222
|
|
|
223
223
|
export interface ProductLinkProps {
|
|
224
224
|
product: Product
|
|
225
|
+
hideFavoriteAction?: boolean
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
// Composed ProductLink component
|
|
228
|
-
function ProductLink({product}: ProductLinkProps) {
|
|
229
|
+
function ProductLink({product, hideFavoriteAction = false}: ProductLinkProps) {
|
|
229
230
|
const {navigateToProduct} = useShopNavigation()
|
|
230
231
|
const {saveProduct, unsaveProduct} = useSavedProductsActions()
|
|
231
232
|
|
|
@@ -247,7 +248,6 @@ function ProductLink({product}: ProductLinkProps) {
|
|
|
247
248
|
|
|
248
249
|
const averageRating = reviewAnalytics?.averageRating
|
|
249
250
|
const reviewCount = reviewAnalytics?.reviewCount
|
|
250
|
-
const currencyCode = price?.currencyCode
|
|
251
251
|
const amount = price?.amount
|
|
252
252
|
? formatMoney(price?.amount, price?.currencyCode)
|
|
253
253
|
: undefined
|
|
@@ -256,7 +256,6 @@ function ProductLink({product}: ProductLinkProps) {
|
|
|
256
256
|
const compareAtPriceAmount = compareAtPrice?.amount
|
|
257
257
|
? formatMoney(compareAtPrice?.amount, compareAtPrice?.currencyCode)
|
|
258
258
|
: undefined
|
|
259
|
-
const compareAtPriceCurrencyCode = compareAtPrice?.currencyCode
|
|
260
259
|
const hasDiscount = compareAtPriceAmount && compareAtPriceAmount !== amount
|
|
261
260
|
|
|
262
261
|
const handlePress = React.useCallback(() => {
|
|
@@ -360,10 +359,12 @@ function ProductLink({product}: ProductLinkProps) {
|
|
|
360
359
|
</ProductLinkPrice>
|
|
361
360
|
</ProductLinkInfo>
|
|
362
361
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
362
|
+
{hideFavoriteAction ? null : (
|
|
363
|
+
<ProductLinkActions
|
|
364
|
+
filled={isFavoritedLocal}
|
|
365
|
+
onPress={handleActionPress}
|
|
366
|
+
/>
|
|
367
|
+
)}
|
|
367
368
|
</ProductLinkRoot>
|
|
368
369
|
)
|
|
369
370
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {createContext, useContext, useState, useCallback} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SearchIcon, X} from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import {useProductSearch} from '../../hooks/product/useProductSearch'
|
|
7
|
+
import {cn} from '../../lib/utils'
|
|
8
|
+
import {type Product} from '../../types'
|
|
9
|
+
import {IconButton} from '../atoms/icon-button'
|
|
10
|
+
import {List} from '../atoms/list'
|
|
11
|
+
import {Input} from '../ui/input'
|
|
12
|
+
|
|
13
|
+
import {ProductLink} from './product-link'
|
|
14
|
+
import {ProductLinkSkeleton} from './product-link-skeleton'
|
|
15
|
+
|
|
16
|
+
const ESTIMATED_PRODUCT_LINK_HEIGHT = 100
|
|
17
|
+
|
|
18
|
+
interface SearchContextValue {
|
|
19
|
+
query: string
|
|
20
|
+
setQuery: (query: string) => void
|
|
21
|
+
products: Product[] | null
|
|
22
|
+
loading: boolean
|
|
23
|
+
error: Error | null
|
|
24
|
+
fetchMore?: () => void
|
|
25
|
+
hasNextPage: boolean
|
|
26
|
+
isTyping: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SearchContext = createContext<SearchContextValue | null>(null)
|
|
30
|
+
|
|
31
|
+
function useSearchContext() {
|
|
32
|
+
const context = useContext(SearchContext)
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error('useSearchContext must be used within a SearchProvider')
|
|
35
|
+
}
|
|
36
|
+
return context
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SearchProviderProps {
|
|
40
|
+
initialQuery?: string
|
|
41
|
+
children: React.ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function SearchProvider({initialQuery = '', children}: SearchProviderProps) {
|
|
45
|
+
const [query, setQueryState] = useState(initialQuery)
|
|
46
|
+
|
|
47
|
+
const {products, loading, error, fetchMore, hasNextPage, isTyping} =
|
|
48
|
+
useProductSearch({
|
|
49
|
+
query,
|
|
50
|
+
fetchPolicy: 'network-only',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const setQuery = useCallback((newQuery: string) => {
|
|
54
|
+
setQueryState(newQuery)
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
const contextValue: SearchContextValue = {
|
|
58
|
+
query,
|
|
59
|
+
setQuery,
|
|
60
|
+
products,
|
|
61
|
+
loading,
|
|
62
|
+
error,
|
|
63
|
+
fetchMore,
|
|
64
|
+
hasNextPage,
|
|
65
|
+
isTyping,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<SearchContext.Provider value={contextValue}>
|
|
70
|
+
{children}
|
|
71
|
+
</SearchContext.Provider>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SearchInputProps {
|
|
76
|
+
placeholder?: string
|
|
77
|
+
className?: string
|
|
78
|
+
inputProps?: React.ComponentProps<'input'>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function SearchInput({
|
|
82
|
+
placeholder = 'Search products...',
|
|
83
|
+
className,
|
|
84
|
+
inputProps,
|
|
85
|
+
}: SearchInputProps) {
|
|
86
|
+
const {query, setQuery} = useSearchContext()
|
|
87
|
+
|
|
88
|
+
const handleQueryChange = useCallback(
|
|
89
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
90
|
+
setQuery(event.target.value)
|
|
91
|
+
inputProps?.onChange?.(event)
|
|
92
|
+
},
|
|
93
|
+
[inputProps, setQuery]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="relative flex flex-1 items-center rounded-full pl-4 pr-2 py-1 bg-gray-100">
|
|
98
|
+
<div className="relative flex items-center">
|
|
99
|
+
<SearchIcon
|
|
100
|
+
size={18}
|
|
101
|
+
className={cn('text-accent-foreground opacity-60')}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="relative flex-1 flex items-center mx-2">
|
|
105
|
+
<Input
|
|
106
|
+
name="search"
|
|
107
|
+
onChange={handleQueryChange}
|
|
108
|
+
placeholder={placeholder}
|
|
109
|
+
type="search"
|
|
110
|
+
role="searchbox"
|
|
111
|
+
autoComplete="off"
|
|
112
|
+
value={query}
|
|
113
|
+
data-testid="search-input"
|
|
114
|
+
{...inputProps}
|
|
115
|
+
className={cn(
|
|
116
|
+
`w-full flex overflow-hidden rounded-radius-28 border-none py-4 px-0 text-text placeholder:text-text placeholder:opacity-60`,
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="relative flex items-center">
|
|
122
|
+
{query === '' ? null : (
|
|
123
|
+
<IconButton
|
|
124
|
+
Icon={X}
|
|
125
|
+
size="sm"
|
|
126
|
+
filled={false}
|
|
127
|
+
iconStyles=""
|
|
128
|
+
onClick={() => setQuery('')}
|
|
129
|
+
buttonStyles="flex items-center rounded-radius-max bg-[var(--grayscale-l20)]"
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface SearchResultsListProps {
|
|
138
|
+
renderItem?: (product: Product, index: number) => React.ReactNode
|
|
139
|
+
height?: number
|
|
140
|
+
itemHeight?: number
|
|
141
|
+
initialStateComponent?: React.JSX.Element
|
|
142
|
+
showScrollbar?: boolean
|
|
143
|
+
overscanCount?: number
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function SearchResultsList({
|
|
147
|
+
height = window.innerHeight,
|
|
148
|
+
renderItem,
|
|
149
|
+
itemHeight = ESTIMATED_PRODUCT_LINK_HEIGHT,
|
|
150
|
+
initialStateComponent,
|
|
151
|
+
showScrollbar,
|
|
152
|
+
overscanCount = 5,
|
|
153
|
+
}: SearchResultsListProps) {
|
|
154
|
+
const {query, products, loading, fetchMore, hasNextPage, isTyping} =
|
|
155
|
+
useSearchContext()
|
|
156
|
+
|
|
157
|
+
const _renderItem = (product: Product, index: number) => {
|
|
158
|
+
if (renderItem) {
|
|
159
|
+
return renderItem(product, index)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="p-2">
|
|
164
|
+
<ProductLink key={product.id} product={product} hideFavoriteAction />
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const shouldShowStartingState = query.trim().length === 0
|
|
170
|
+
const shouldShowLoading =
|
|
171
|
+
(!products || products.length === 0) && (loading || isTyping)
|
|
172
|
+
const shouldShowEmptyState = (!products || products.length === 0) && !loading
|
|
173
|
+
|
|
174
|
+
if (shouldShowStartingState) {
|
|
175
|
+
return (
|
|
176
|
+
initialStateComponent || (
|
|
177
|
+
<div className="flex items-center justify-center h-32 text-gray-500">
|
|
178
|
+
Start typing to search for products
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (shouldShowLoading) {
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex flex-col px-4 py-4">
|
|
187
|
+
<ProductLinkSkeleton className="mb-4" />
|
|
188
|
+
<ProductLinkSkeleton className="mb-4" />
|
|
189
|
+
<ProductLinkSkeleton className="mb-4" />
|
|
190
|
+
<ProductLinkSkeleton className="mb-4" />
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (shouldShowEmptyState) {
|
|
196
|
+
return (
|
|
197
|
+
<div className="flex items-center justify-center h-32 text-gray-500">
|
|
198
|
+
{`No products found for "${query}"`}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<List
|
|
205
|
+
items={products || []}
|
|
206
|
+
height={height}
|
|
207
|
+
renderItem={_renderItem}
|
|
208
|
+
itemSizeForRow={() => itemHeight}
|
|
209
|
+
fetchMore={hasNextPage ? fetchMore : undefined}
|
|
210
|
+
showScrollbar={showScrollbar}
|
|
211
|
+
overscanCount={overscanCount}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface SearchProviderPropsWithoutChildren
|
|
217
|
+
extends Omit<SearchProviderProps, 'children'> {}
|
|
218
|
+
export interface SearchResultsProps
|
|
219
|
+
extends SearchProviderPropsWithoutChildren,
|
|
220
|
+
SearchInputProps,
|
|
221
|
+
SearchResultsListProps {
|
|
222
|
+
showSearchInput?: boolean
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function Search({
|
|
226
|
+
initialQuery,
|
|
227
|
+
placeholder,
|
|
228
|
+
inputProps,
|
|
229
|
+
height,
|
|
230
|
+
className,
|
|
231
|
+
renderItem,
|
|
232
|
+
itemHeight,
|
|
233
|
+
}: SearchResultsProps) {
|
|
234
|
+
const _renderItem = (product: Product, index: number) => {
|
|
235
|
+
if (renderItem) {
|
|
236
|
+
return renderItem(product, index)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="p-2">
|
|
241
|
+
<ProductLink key={product.id} product={product} hideFavoriteAction />
|
|
242
|
+
</div>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<SearchProvider initialQuery={initialQuery}>
|
|
248
|
+
<div className={cn('flex flex-col ', className)}>
|
|
249
|
+
<div className="fixed top-0 left-0 right-0 p-4 w-full z-20 bg-background">
|
|
250
|
+
<SearchInput placeholder={placeholder} inputProps={inputProps} />
|
|
251
|
+
</div>
|
|
252
|
+
<div className="h-14" />
|
|
253
|
+
<SearchResultsList
|
|
254
|
+
height={height}
|
|
255
|
+
renderItem={_renderItem}
|
|
256
|
+
itemHeight={itemHeight}
|
|
257
|
+
showScrollbar
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
</SearchProvider>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export {SearchProvider, SearchInput, SearchResultsList, Search}
|
|
@@ -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
|
@@ -6,6 +6,9 @@ export * from './commerce/merchant-card'
|
|
|
6
6
|
export * from './commerce/product-card-skeleton'
|
|
7
7
|
export * from './commerce/merchant-card-skeleton'
|
|
8
8
|
export * from './commerce/quantity-selector'
|
|
9
|
+
export * from './commerce/search'
|
|
10
|
+
|
|
11
|
+
export * from './content/content-wrapper'
|
|
9
12
|
|
|
10
13
|
export * from './navigation/transition-container'
|
|
11
14
|
export * from './navigation/transition-link'
|
|
@@ -9,7 +9,7 @@ function Input({className, type, ...props}: React.ComponentProps<'input'>) {
|
|
|
9
9
|
data-slot="input"
|
|
10
10
|
className={cn(
|
|
11
11
|
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
12
|
-
'focus-
|
|
12
|
+
'focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:outline-none',
|
|
13
13
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
14
14
|
className
|
|
15
15
|
)}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {useCallback} from 'react'
|
|
1
|
+
import {useCallback, useState} from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
ContentVisibility,
|
|
5
|
+
Content,
|
|
6
|
+
ContentCreateUserErrors,
|
|
7
|
+
} from '@shopify/shop-minis-platform'
|
|
5
8
|
|
|
6
9
|
import {useHandleAction} from '../../internal/useHandleAction'
|
|
7
10
|
import {useShopActions} from '../../internal/useShopActions'
|
|
@@ -20,15 +23,22 @@ interface UseCreateImageContentReturns {
|
|
|
20
23
|
*/
|
|
21
24
|
createImageContent: (
|
|
22
25
|
params: CreateImageContentParams
|
|
23
|
-
) => Promise<
|
|
26
|
+
) => Promise<{data: Content; userErrors?: ContentCreateUserErrors[]}>
|
|
27
|
+
/**
|
|
28
|
+
* Whether the content is being created.
|
|
29
|
+
*/
|
|
30
|
+
loading: boolean
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export const useCreateImageContent = (): UseCreateImageContentReturns => {
|
|
27
34
|
const {createContent} = useShopActions()
|
|
28
35
|
const {uploadImage} = useImageUpload()
|
|
36
|
+
const [loading, setLoading] = useState(false)
|
|
29
37
|
|
|
30
38
|
const createImageContent = useCallback(
|
|
31
39
|
async (params: CreateImageContentParams) => {
|
|
40
|
+
setLoading(true)
|
|
41
|
+
|
|
32
42
|
const {image, contentTitle, visibility} = params
|
|
33
43
|
|
|
34
44
|
if (!image.type) {
|
|
@@ -50,16 +60,21 @@ export const useCreateImageContent = (): UseCreateImageContentReturns => {
|
|
|
50
60
|
throw new Error('Image upload failed')
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
const createContentResult = await createContent({
|
|
54
64
|
title: contentTitle,
|
|
55
65
|
imageUrl: uploadImageUrl,
|
|
56
66
|
visibility,
|
|
57
67
|
})
|
|
68
|
+
|
|
69
|
+
setLoading(false)
|
|
70
|
+
|
|
71
|
+
return createContentResult
|
|
58
72
|
},
|
|
59
73
|
[createContent, uploadImage]
|
|
60
74
|
)
|
|
61
75
|
|
|
62
76
|
return {
|
|
63
77
|
createImageContent: useHandleAction(createImageContent),
|
|
78
|
+
loading,
|
|
64
79
|
}
|
|
65
80
|
}
|
|
@@ -36,6 +36,10 @@ interface UseProductSearchReturns extends PaginatedDataHookReturnsBase {
|
|
|
36
36
|
* The products returned from the query.
|
|
37
37
|
*/
|
|
38
38
|
products: Product[] | null
|
|
39
|
+
/**
|
|
40
|
+
* Whether the user is typing.
|
|
41
|
+
*/
|
|
42
|
+
isTyping: boolean
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export const useProductSearch = (
|
|
@@ -85,11 +89,16 @@ export const useProductSearch = (
|
|
|
85
89
|
)
|
|
86
90
|
|
|
87
91
|
const products = useMemo(() => {
|
|
92
|
+
if (debouncedQuery.trim().length === 0) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
return data ?? null
|
|
89
|
-
}, [data])
|
|
97
|
+
}, [data, debouncedQuery])
|
|
90
98
|
|
|
91
99
|
return {
|
|
92
100
|
...rest,
|
|
93
101
|
products,
|
|
102
|
+
isTyping: debouncedQuery !== query,
|
|
94
103
|
}
|
|
95
104
|
}
|
package/src/mocks.ts
CHANGED
|
@@ -422,6 +422,21 @@ function makeMockActions(): ShopActions {
|
|
|
422
422
|
products: null,
|
|
423
423
|
},
|
|
424
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
|
+
},
|
|
425
440
|
} as const
|
|
426
441
|
|
|
427
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>
|