11import React , { useState } from 'react' ;
2- import { ShoppingCart , Zap , Store } from 'lucide-react' ;
32import { isShopifyConfigured } from './utils/shopify' ;
43import ShopifySetupGuide from './components/ShopifySetupGuide' ;
54import { useProducts } from './hooks/useShopify' ;
6- import { useCartContext } from './context/CartContext' ;
7- import { formatPrice , createCheckoutPermalink } from './utils/shopify' ;
5+ import { useCartContext } from './hooks/useCartContext' ;
86import CartDrawer from './components/CartDrawer' ;
7+ import Header from './components/Header' ;
8+ import ProductImageGallery from './components/ProductImageGallery' ;
9+ import ProductDetails from './components/ProductDetails' ;
10+ import { LoadingState , ErrorState , EmptyState } from './components/StateComponents' ;
11+ import { ShopifyProduct } from './types/shopify' ;
912
1013const App : React . FC = ( ) => {
1114 const [ isCartOpen , setIsCartOpen ] = useState ( false ) ;
1215 const { products, loading, error } = useProducts ( ) ;
13- const { addToCart , cart } = useCartContext ( ) ;
16+ const { cart } = useCartContext ( ) ;
1417
1518 const [ selectedVariant , setSelectedVariant ] = useState ( 0 ) ;
1619 const [ selectedImage , setSelectedImage ] = useState ( 0 ) ;
17- const [ quantity , setQuantity ] = useState ( 1 ) ;
18- const [ isAddingToCart , setIsAddingToCart ] = useState ( false ) ;
19- const [ isBuyingNow , setIsBuyingNow ] = useState ( false ) ;
20- const [ randomProduct , setRandomProduct ] = useState ( null ) ;
20+ const [ randomProduct , setRandomProduct ] = useState < ShopifyProduct | null > ( null ) ;
2121
2222 React . useEffect ( ( ) => {
2323 if ( products . length > 0 && ! randomProduct ) {
@@ -31,249 +31,41 @@ const App: React.FC = () => {
3131 }
3232
3333 if ( loading && ! randomProduct ) {
34- return (
35- < div className = "min-h-screen flex items-center justify-center" >
36- < div className = "animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" > </ div >
37- </ div >
38- ) ;
34+ return < LoadingState /> ;
3935 }
4036
4137 if ( error ) {
42- return (
43- < div className = "min-h-screen flex items-center justify-center" >
44- < div className = "text-center" >
45- < div className = "text-red-600 mb-4" > ⚠️</ div >
46- < h3 className = "text-lg font-medium text-gray-900 mb-2" > Something went wrong</ h3 >
47- < p className = "text-gray-600" > { error } </ p >
48- </ div >
49- </ div >
50- ) ;
38+ return < ErrorState error = { error } /> ;
5139 }
5240
5341 if ( ! randomProduct ) {
54- return (
55- < div className = "min-h-screen flex items-center justify-center" >
56- < div className = "text-center" >
57- < Store className = "h-16 w-16 text-gray-300 mx-auto mb-4" />
58- < h3 className = "text-xl font-medium text-gray-600 mb-2" > No Products Found</ h3 >
59- < p className = "text-gray-500" > Please add products to your Shopify store.</ p >
60- </ div >
61- </ div >
62- ) ;
42+ return < EmptyState /> ;
6343 }
6444
65- const currentVariant = randomProduct . variants . nodes [ selectedVariant ] ;
66- const isProductAvailable = currentVariant ?. availableForSale && randomProduct . availableForSale ;
67- const isOnSale = currentVariant ?. compareAtPrice &&
68- parseFloat ( currentVariant . compareAtPrice . amount ) > parseFloat ( currentVariant . price . amount ) ;
69-
70- const handleAddToCart = async ( ) => {
71- if ( ! currentVariant || ! isProductAvailable ) return ;
72-
73- setIsAddingToCart ( true ) ;
74- try {
75- await addToCart ( [
76- {
77- merchandiseId : currentVariant . id ,
78- quantity,
79- } ,
80- ] ) ;
81- } catch ( error ) {
82- console . error ( 'Failed to add to cart:' , error ) ;
83- } finally {
84- setIsAddingToCart ( false ) ;
85- }
86- } ;
87-
88- const handleBuyNow = async ( ) => {
89- if ( ! currentVariant || ! isProductAvailable ) return ;
90-
91- setIsBuyingNow ( true ) ;
92- try {
93- const checkoutUrl = createCheckoutPermalink ( currentVariant . id , quantity ) ;
94-
95- const isInIframe = window . self !== window . top ;
96-
97- if ( isInIframe ) {
98- window . open ( checkoutUrl , '_blank' , 'noopener,noreferrer' ) ;
99- } else {
100- window . location . href = checkoutUrl ;
101- }
102- } catch ( error ) {
103- console . error ( 'Failed to create buy now link:' , error ) ;
104- } finally {
105- setIsBuyingNow ( false ) ;
106- }
107- } ;
108-
10945 return (
11046 < div className = "min-h-screen bg-gray-50" >
111- < header className = "bg-white shadow-sm sticky top-0 z-50" >
112- < div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" >
113- < div className = "flex items-center justify-between h-16" >
114- < div className = "flex-shrink-0" >
115- < div className = "text-2xl font-bold text-gray-900" >
116- Mock< span className = "text-blue-600" > Store</ span >
117- </ div >
118- </ div >
119- < button
120- onClick = { ( ) => setIsCartOpen ( true ) }
121- className = "relative p-2 text-gray-700 hover:text-blue-600 transition-colors"
122- >
123- < ShoppingCart className = "h-6 w-6" />
124- { cart && cart . totalQuantity > 0 && (
125- < span className = "absolute -top-1 -right-1 bg-blue-600 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center" >
126- { cart . totalQuantity }
127- </ span >
128- ) }
129- </ button >
130- </ div >
131- </ div >
132- </ header >
47+ < Header
48+ cartQuantity = { cart ?. totalQuantity || 0 }
49+ onCartClick = { ( ) => setIsCartOpen ( true ) }
50+ />
13351
13452 < div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" >
13553 < div className = "grid grid-cols-1 lg:grid-cols-2 gap-12" >
136- < div className = "space-y-4" >
137- < div className = "aspect-w-1 aspect-h-1 bg-gray-200 rounded-lg overflow-hidden" >
138- { randomProduct . images . nodes [ selectedImage ] ? (
139- < img
140- src = { randomProduct . images . nodes [ selectedImage ] . url }
141- alt = { randomProduct . images . nodes [ selectedImage ] . altText || randomProduct . title }
142- className = "w-full h-96 lg:h-[500px] object-cover"
143- />
144- ) : (
145- < div className = "w-full h-96 lg:h-[500px] bg-gray-200 flex items-center justify-center" >
146- < span className = "text-gray-400" > No image</ span >
147- </ div >
148- ) }
149- </ div >
150-
151- { randomProduct . images . nodes . length > 1 && (
152- < div className = "flex space-x-2 overflow-x-auto" >
153- { randomProduct . images . nodes . map ( ( image , index ) => (
154- < button
155- key = { image . id }
156- onClick = { ( ) => setSelectedImage ( index ) }
157- className = { `flex-shrink-0 w-20 h-20 rounded-md overflow-hidden border-2 transition-colors ${
158- selectedImage === index ? 'border-blue-600' : 'border-gray-200'
159- } `}
160- >
161- < img
162- src = { image . url }
163- alt = { image . altText || randomProduct . title }
164- className = "w-full h-full object-cover"
165- />
166- </ button >
167- ) ) }
168- </ div >
169- ) }
170- </ div >
171-
172- < div className = "space-y-6" >
173- { randomProduct . vendor && (
174- < p className = "text-sm text-blue-600 font-medium" > { randomProduct . vendor } </ p >
175- ) }
176-
177- < h1 className = "text-3xl lg:text-4xl font-bold text-gray-900" >
178- { randomProduct . title }
179- </ h1 >
180-
181- < div className = "space-y-2" >
182- < div className = "flex items-center space-x-3" >
183- < span className = "text-3xl font-bold text-gray-900" >
184- { formatPrice ( currentVariant . price ) }
185- </ span >
186- { isOnSale && currentVariant . compareAtPrice && (
187- < span className = "text-xl text-gray-500 line-through" >
188- { formatPrice ( currentVariant . compareAtPrice ) }
189- </ span >
190- ) }
191- { isOnSale && (
192- < span className = "bg-red-100 text-red-800 px-2 py-1 rounded-md text-sm font-medium" >
193- Sale
194- </ span >
195- ) }
196- </ div >
197- { ! isProductAvailable && (
198- < p className = "text-red-600 font-medium" > Out of Stock</ p >
199- ) }
200- </ div >
201-
202- { randomProduct . description && (
203- < div className = "prose prose-sm max-w-none" >
204- < p className = "text-gray-600" > { randomProduct . description } </ p >
205- </ div >
206- ) }
207-
208- { randomProduct . variants . nodes . length > 1 && (
209- < div className = "space-y-4" >
210- < h3 className = "text-lg font-medium text-gray-900" > Options</ h3 >
211- < div className = "grid grid-cols-2 sm:grid-cols-3 gap-2" >
212- { randomProduct . variants . nodes . map ( ( variant , index ) => (
213- < button
214- key = { variant . id }
215- onClick = { ( ) => setSelectedVariant ( index ) }
216- className = { `p-3 text-sm font-medium rounded-md border transition-colors ${
217- selectedVariant === index
218- ? 'border-blue-600 bg-blue-50 text-blue-600'
219- : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
220- } `}
221- >
222- { variant . title }
223- </ button >
224- ) ) }
225- </ div >
226- </ div >
227- ) }
228-
229- < div className = "space-y-2" >
230- < label className = "text-lg font-medium text-gray-900" > Quantity</ label >
231- < div className = "flex items-center space-x-3" >
232- < button
233- onClick = { ( ) => setQuantity ( Math . max ( 1 , quantity - 1 ) ) }
234- className = "w-10 h-10 rounded-md border border-gray-300 flex items-center justify-center hover:bg-gray-50 transition-colors"
235- >
236- -
237- </ button >
238- < span className = "w-12 text-center font-medium" > { quantity } </ span >
239- < button
240- onClick = { ( ) => setQuantity ( quantity + 1 ) }
241- className = "w-10 h-10 rounded-md border border-gray-300 flex items-center justify-center hover:bg-gray-50 transition-colors"
242- >
243- +
244- </ button >
245- </ div >
246- </ div >
247-
248- < div className = "space-y-4" >
249- < div className = "flex space-x-4" >
250- < button
251- onClick = { handleAddToCart }
252- disabled = { ! isProductAvailable || isAddingToCart }
253- className = { `flex-1 py-3 px-6 rounded-md font-medium transition-colors flex items-center justify-center ${
254- ! isProductAvailable || isAddingToCart
255- ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
256- : 'bg-blue-600 text-white hover:bg-blue-700'
257- } `}
258- >
259- < ShoppingCart className = "h-5 w-5 mr-2" />
260- { isAddingToCart ? 'Adding...' : ! isProductAvailable ? 'Out of Stock' : 'Add to Cart' }
261- </ button >
262- < button
263- onClick = { handleBuyNow }
264- disabled = { ! isProductAvailable || isBuyingNow }
265- className = { `flex-1 py-3 px-6 rounded-md font-medium transition-colors flex items-center justify-center border-2 ${
266- ! isProductAvailable || isBuyingNow
267- ? 'border-gray-300 text-gray-500 cursor-not-allowed bg-gray-50'
268- : 'border-blue-600 text-blue-600 hover:bg-blue-600 hover:text-white bg-white'
269- } `}
270- >
271- < Zap className = "h-5 w-5 mr-2" />
272- { isBuyingNow ? 'Redirecting...' : ! isProductAvailable ? 'Out of Stock' : 'Buy Now' }
273- </ button >
274- </ div >
275- </ div >
276- </ div >
54+ < ProductImageGallery
55+ images = { randomProduct . images . nodes }
56+ productTitle = { randomProduct . title }
57+ selectedImage = { selectedImage }
58+ onImageSelect = { setSelectedImage }
59+ />
60+
61+ < ProductDetails
62+ vendor = { randomProduct . vendor }
63+ title = { randomProduct . title }
64+ description = { randomProduct . description }
65+ variants = { randomProduct . variants . nodes }
66+ selectedVariant = { selectedVariant }
67+ onVariantChange = { setSelectedVariant }
68+ />
27769 </ div >
27870 </ div >
27971
0 commit comments