Skip to content

Commit cd9b060

Browse files
committed
refactor: more minimal
1 parent 8956690 commit cd9b060

File tree

18 files changed

+441
-1351
lines changed

18 files changed

+441
-1351
lines changed

shopify-vite-react-ts/.bolt/prompt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Shopify-Powered Vite React TS Storefront Template
22

3-
The code in this starter template is an example fully-featured Shopify storefront built with Vite, React, and TypeScript. The meat of the functionality is in the useShopify hook, which is a wrapper around the Shopify Tokenless API.
3+
The code in this starter template is an example Shopify-powered storefront built with Vite, React, and TypeScript. The meat of the functionality is in the useShopify hook, which is a wrapper around the Shopify Tokenless API and can be used to build out additional features.
44

55
## Tokenless API
66

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

Lines changed: 267 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,283 @@
11
import React, { useState } from 'react';
2-
import { Routes, Route } from 'react-router-dom';
3-
import Header from './components/Header';
2+
import { ShoppingCart, Zap, Store } from 'lucide-react';
3+
import { isShopifyConfigured } from './utils/shopify';
4+
import ShopifySetupGuide from './components/ShopifySetupGuide';
5+
import { useProducts } from './hooks/useShopify';
6+
import { useCartContext } from './context/CartContext';
7+
import { formatPrice, createCheckoutPermalink } from './utils/shopify';
48
import CartDrawer from './components/CartDrawer';
5-
import HomePage from './pages/HomePage';
6-
import ProductPage from './pages/ProductPage';
7-
import CategoriesPage from './pages/CategoriesPage';
8-
import CategoryPage from './pages/CategoryPage';
99

1010
const App: React.FC = () => {
11-
const [searchQuery, setSearchQuery] = useState('');
1211
const [isCartOpen, setIsCartOpen] = useState(false);
12+
const { products, loading, error } = useProducts();
13+
const { addToCart, cart } = useCartContext();
14+
15+
const [selectedVariant, setSelectedVariant] = useState(0);
16+
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);
1321

14-
const handleSearchChange = (query: string) => {
15-
setSearchQuery(query);
16-
};
22+
React.useEffect(() => {
23+
if (products.length > 0 && !randomProduct) {
24+
const randomIndex = Math.floor(Math.random() * products.length);
25+
setRandomProduct(products[randomIndex]);
26+
}
27+
}, [products, randomProduct]);
28+
29+
if (!isShopifyConfigured()) {
30+
return <ShopifySetupGuide />;
31+
}
32+
33+
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+
);
39+
}
40+
41+
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+
);
51+
}
52+
53+
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+
);
63+
}
64+
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);
1769

18-
const handleCartClick = () => {
19-
setIsCartOpen(true);
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+
}
2086
};
2187

22-
const handleCartClose = () => {
23-
setIsCartOpen(false);
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+
}
24107
};
25108

26109
return (
27110
<div className="min-h-screen bg-gray-50">
28-
<Header
29-
searchQuery={searchQuery}
30-
onSearchChange={handleSearchChange}
31-
onCartClick={handleCartClick}
32-
/>
33-
34-
<main>
35-
<Routes>
36-
<Route
37-
path="/"
38-
element={
39-
<HomePage
40-
searchQuery={searchQuery}
41-
onSearchChange={handleSearchChange}
42-
/>
43-
}
44-
/>
45-
<Route path="/products/:handle" element={<ProductPage />} />
46-
<Route path="/categories" element={<CategoriesPage />} />
47-
<Route path="/category/:handle" element={<CategoryPage />} />
48-
</Routes>
49-
</main>
50-
51-
<CartDrawer isOpen={isCartOpen} onClose={handleCartClose} />
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>
133+
134+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
135+
<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>
277+
</div>
278+
</div>
279+
280+
<CartDrawer isOpen={isCartOpen} onClose={() => setIsCartOpen(false)} />
52281
</div>
53282
);
54283
};

0 commit comments

Comments
 (0)