Skip to content

Commit 11be89c

Browse files
committed
feat: enhance product display and cart functionality
- Refactored App component to utilize new ProductDetails, ProductImageGallery, and Header components for improved structure and readability. - Introduced QuantitySelector for managing product quantities. - Added loading, error, and empty state components for better user experience. - Updated CartContext to support addProductToCart and buyNow functionalities. - Improved .gitignore to exclude unnecessary editor files.
1 parent cd9b060 commit 11be89c

File tree

12 files changed

+413
-276
lines changed

12 files changed

+413
-276
lines changed

shopify-vite-react-ts/.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Logs
21
logs
32
*.log
43
npm-debug.log*
@@ -12,7 +11,6 @@ dist
1211
dist-ssr
1312
*.local
1413

15-
# Editor directories and files
1614
.vscode/*
1715
!.vscode/extensions.json
1816
.idea

shopify-vite-react-ts/src/App.tsx

Lines changed: 30 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import React, { useState } from 'react';
2-
import { ShoppingCart, Zap, Store } from 'lucide-react';
32
import { isShopifyConfigured } from './utils/shopify';
43
import ShopifySetupGuide from './components/ShopifySetupGuide';
54
import { useProducts } from './hooks/useShopify';
6-
import { useCartContext } from './context/CartContext';
7-
import { formatPrice, createCheckoutPermalink } from './utils/shopify';
5+
import { useCartContext } from './hooks/useCartContext';
86
import 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

1013
const 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

shopify-vite-react-ts/src/components/CartDrawer.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { X, Plus, Minus, ShoppingBag } from 'lucide-react';
3-
import { useCartContext } from '../context/CartContext';
3+
import { useCartContext } from '../hooks/useCartContext';
44
import { formatPrice } from '../utils/shopify';
55

66
interface CartDrawerProps {
@@ -21,19 +21,15 @@ const CartDrawer: React.FC<CartDrawerProps> = ({ isOpen, onClose }) => {
2121

2222
const handleCheckout = () => {
2323
if (cart?.checkoutUrl) {
24-
// Check if we're within an iframe
2524
const isInIframe = window.self !== window.top;
2625

2726
if (isInIframe) {
28-
// Open checkout in a new window/tab when in iframe
2927
window.open(cart.checkoutUrl, '_blank', 'noopener,noreferrer');
3028
} else {
31-
// Open checkout in the same window when not in iframe
3229
window.location.href = cart.checkoutUrl;
3330
}
3431
} else {
3532
console.error('No checkout URL available');
36-
// Optionally show an error message to the user
3733
}
3834
};
3935

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { ShoppingCart } from 'lucide-react';
3+
4+
interface HeaderProps {
5+
cartQuantity: number;
6+
onCartClick: () => void;
7+
}
8+
9+
const Header: React.FC<HeaderProps> = ({ cartQuantity, onCartClick }) => {
10+
return (
11+
<header className="bg-white shadow-sm sticky top-0 z-50">
12+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
13+
<div className="flex items-center justify-between h-16">
14+
<div className="flex-shrink-0">
15+
<div className="text-2xl font-bold text-gray-900">
16+
Mock<span className="text-blue-600">Store</span>
17+
</div>
18+
</div>
19+
<button
20+
onClick={onCartClick}
21+
className="relative p-2 text-gray-700 hover:text-blue-600 transition-colors"
22+
>
23+
<ShoppingCart className="h-6 w-6" />
24+
{cartQuantity > 0 && (
25+
<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">
26+
{cartQuantity}
27+
</span>
28+
)}
29+
</button>
30+
</div>
31+
</div>
32+
</header>
33+
);
34+
};
35+
36+
export default Header;

0 commit comments

Comments
 (0)