Skip to content

Commit 2737f67

Browse files
committed
add problem formulation
1 parent 17af983 commit 2737f67

File tree

2 files changed

+140
-44
lines changed

2 files changed

+140
-44
lines changed

exercises/03.data-fetching/05.problem.product-variations/app/routes/_landing.products.$productId.tsx

Lines changed: 130 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import {
99
Minus,
1010
} from 'lucide-react'
1111
import { useState } from 'react'
12-
import { Link } from 'react-router'
12+
import { Link, useSearchParams } from 'react-router'
1313
import { ProductCard } from '#app/components/product-card.js'
1414
import {
1515
getProductById,
1616
getRelatedProducts,
17-
} from '#app/domain/products.server.ts'
17+
} from '#app/domain/products.server.js'
1818
import {
1919
getMetaFromMatches,
2020
getMetaTitle,
@@ -32,30 +32,61 @@ export const meta: Route.MetaFunction = ({ matches }) => {
3232
]
3333
}
3434

35-
export const loader = async ({ params }: Route.LoaderArgs) => {
35+
export const loader = async ({ params, request }: Route.LoaderArgs) => {
3636
const { productId } = params
3737
const product = await getProductById(productId)
3838
const relatedProducts = await getRelatedProducts(
3939
productId,
4040
product?.category.id,
4141
product?.brand.id,
4242
)
43-
43+
const uniqueSizes = Array.from(
44+
new Set(product?.variations.map((v) => v.size) || []),
45+
)
46+
const uniqueColors = Array.from(
47+
new Set(product?.variations.map((v) => v.color) || []),
48+
)
49+
const url = new URL(request.url)
50+
const searchParams = url.searchParams
51+
// 💰 We need to find the selectColor and selectedSize from the url search params
52+
const selectedSize = undefined
53+
const selectedColor = undefined
54+
// 💰 Here's the code to determine available sizes and colors!
55+
// const availableSizes = selectedColor
56+
// ? product?.variations
57+
// .filter((v) => v.color === selectedColor && v.quantity > 0)
58+
// .map((v) => v.size) || []
59+
// : uniqueSizes
60+
// const availableColors = selectedSize
61+
// ? product?.variations
62+
// .filter((v) => v.size === selectedSize && v.quantity > 0)
63+
// .map((v) => v.color) || []
64+
// : uniqueColors
65+
// 🐨 Let's determine the selected variation based on the selectedSize and selectedColor
66+
// 💰 You can use the find method on the product.variations array
67+
const selectedVariation = undefined
4468
return {
4569
product,
4670
relatedProducts,
71+
uniqueSizes,
72+
uniqueColors,
73+
// 💰 We need to return availableSizes, availableColors and selectedVariation here
4774
}
4875
}
4976

5077
export default function ProductDetailPage({
5178
loaderData,
5279
}: Route.ComponentProps) {
53-
const { product, relatedProducts } = loaderData
54-
const [selectedSize] = useState('')
55-
const [selectedColor] = useState('')
80+
// 💣 remove this in favor of loaderData
81+
const selectedVariation = undefined
82+
const { product, relatedProducts, uniqueColors, uniqueSizes } = loaderData
83+
const [searchParams, setSearchParams] = useSearchParams()
84+
5685
const [quantity, setQuantity] = useState(1)
5786
const [activeImage, setActiveImage] = useState(0)
58-
87+
// 💰 We need to get size and color from the URL search params
88+
const selectedSize = undefined
89+
const selectedColor = undefined
5990
if (!product) {
6091
return (
6192
<div className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-gray-900">
@@ -83,6 +114,50 @@ export default function ProductDetailPage({
83114
alert('Added to cart!')
84115
}
85116

117+
const handleSizeChange = (size: string) => () => {
118+
setSearchParams((prev) => {
119+
const newParams = new URLSearchParams(prev)
120+
// 🐨 Let's either remove or set the size depending if this size was selected or not!
121+
// 💰 You can use size and the selectedSize for the comparison
122+
123+
// 🐨 After that, let's get the color from the url
124+
const color = undefined
125+
// 💰 If the selected color is not available for the new size, remove it
126+
// const noQuantityForColor =
127+
// color &&
128+
// product.variations.some(
129+
// (v) => v.size === size && v.color === color && v.quantity === 0,
130+
// )
131+
//if (noQuantityForColor) {
132+
// newParams.delete('color')
133+
//}
134+
return newParams
135+
})
136+
setQuantity(1)
137+
}
138+
139+
const handleColorChange = (color: string) => () => {
140+
setSearchParams((prev) => {
141+
const newParams = new URLSearchParams(prev)
142+
// 🐨 Let's either remove or set the color depending if this color was selected or not!
143+
// 💰 You can use color and the selectedColor for the comparison
144+
145+
// 🐨 After that, let's get the size from the url
146+
const size = undefined
147+
// 💰 If the selected size is not available for the new size, remove it
148+
// const noQuantityForSize =
149+
// size &&
150+
// product.variations.some(
151+
// (v) => v.size === size && v.size === size && v.quantity === 0,
152+
// )
153+
//if (noQuantityForSize) {
154+
// newParams.delete('size')
155+
//}
156+
return newParams
157+
})
158+
setQuantity(1)
159+
}
160+
86161
// Mock additional images for gallery
87162
const productImages = [
88163
product.imageUrl,
@@ -179,19 +254,23 @@ export default function ProductDetailPage({
179254
Size
180255
</h3>
181256
<div className="grid grid-cols-6 gap-3">
182-
{/* {product.sizes.map((size) => (
257+
{uniqueSizes.map((size) => (
183258
<button
184259
key={size}
185-
onClick={() => setSelectedSize(size)}
186-
className={`rounded-lg border px-4 py-3 text-center transition-colors duration-200 ${
260+
// 🐨 Let's add a title to the button when it's out of stock and disable it!
261+
// 💰 check the availableSizes and if it includes the size
262+
disabled={false}
263+
title={false ? 'Out of stock' : ''}
264+
onClick={handleSizeChange(size)}
265+
className={`rounded-lg border px-4 py-3 text-center transition-colors duration-200 disabled:opacity-20 ${
187266
selectedSize === size
188267
? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
189268
: 'border-gray-300 text-gray-700 hover:border-amber-300 dark:border-gray-600 dark:text-gray-300 dark:hover:border-amber-700'
190269
}`}
191270
>
192271
{size}
193272
</button>
194-
))} */}
273+
))}
195274
</div>
196275
</div>
197276

@@ -200,53 +279,65 @@ export default function ProductDetailPage({
200279
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
201280
Color
202281
</h3>
203-
<div className="flex space-x-3">
204-
{/* {product.colors.map((color) => (
282+
<div className="flex flex-wrap space-x-3">
283+
{uniqueColors.map((color) => (
205284
<button
206285
key={color}
207-
onClick={() => setSelectedColor(color)}
208-
className={`rounded-lg border px-4 py-2 transition-colors duration-200 ${
286+
// 🐨 Let's add a title to the button when it's out of stock and disable it!
287+
// 💰 check the availableColors and if it includes the color
288+
disabled={false}
289+
title={false ? 'Out of stock' : ''}
290+
onClick={handleColorChange(color)}
291+
className={`rounded-lg border px-4 py-2 transition-colors duration-200 disabled:opacity-20 ${
209292
selectedColor === color
210293
? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
211294
: 'border-gray-300 text-gray-700 hover:border-amber-300 dark:border-gray-600 dark:text-gray-300 dark:hover:border-amber-700'
212295
}`}
213296
>
214297
{color}
215298
</button>
216-
))} */}
299+
))}
217300
</div>
218301
</div>
219302

220303
{/* Quantity */}
221-
<div>
222-
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
223-
Quantity
224-
</h3>
225-
<div className="flex items-center space-x-4">
226-
<div className="flex items-center rounded-lg border border-gray-300 dark:border-gray-600">
227-
<button
228-
onClick={() => setQuantity(Math.max(1, quantity - 1))}
229-
className="p-2 transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-700"
230-
>
231-
<Minus className="h-4 w-4" />
232-
</button>
233-
<span className="px-4 py-2 font-medium text-gray-900 dark:text-white">
234-
{quantity}
235-
</span>
236-
<button
237-
onClick={() => setQuantity(quantity + 1)}
238-
className="p-2 transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-700"
239-
>
240-
<Plus className="h-4 w-4" />
241-
</button>
304+
{selectedVariation ? (
305+
<div>
306+
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
307+
Quantity
308+
</h3>
309+
<div className="flex items-center space-x-4">
310+
<div className="flex items-center rounded-lg border border-gray-300 dark:border-gray-600">
311+
<button
312+
onClick={() => setQuantity(Math.max(1, quantity - 1))}
313+
// 🐨 Let's disable this button if the quantity is equal to 1
314+
disabled={false}
315+
className="p-2 transition-colors duration-200 hover:bg-gray-100 disabled:opacity-20 disabled:hover:bg-transparent dark:hover:bg-gray-700"
316+
>
317+
<Minus className="h-4 w-4" />
318+
</button>
319+
<span className="px-4 py-2 font-medium text-gray-900 dark:text-white">
320+
{quantity}
321+
</span>
322+
<button
323+
onClick={() => setQuantity(quantity + 1)}
324+
// 🐨 Let's disable this button if the quantity is equal to the selected variation quantity
325+
disabled={false}
326+
className="p-2 transition-colors duration-200 hover:bg-gray-100 disabled:opacity-20 disabled:hover:bg-transparent dark:hover:bg-gray-700"
327+
>
328+
<Plus className="h-4 w-4" />
329+
</button>
330+
</div>
242331
</div>
243332
</div>
244-
</div>
333+
) : null}
245334

246335
{/* Add to Cart */}
247336
<div className="flex space-x-4">
248337
<button
249338
onClick={handleAddToCart}
339+
// 🐨 Let's disable this button if there is no selectedVariation or it's quantity is equal to 0
340+
disabled={false}
250341
className="flex-1 rounded-lg bg-amber-600 px-6 py-4 font-medium text-white transition-colors duration-300 hover:bg-amber-700 hover:shadow-lg"
251342
>
252343
Add to Cart

exercises/03.data-fetching/05.solution.product-variations/app/routes/_landing.products.$productId.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from 'lucide-react'
1111
import { useState } from 'react'
1212
import { Link, useSearchParams } from 'react-router'
13-
import { ProductCard } from '#app/components/product-card.tsx'
13+
import { ProductCard } from '#app/components/product-card.js'
1414
import {
1515
getProductById,
1616
getRelatedProducts,
@@ -173,7 +173,12 @@ export default function ProductDetailPage({
173173
}
174174

175175
// Mock additional images for gallery
176-
const productImages = [product.imageUrl]
176+
const productImages = [
177+
product.imageUrl,
178+
product.imageUrl,
179+
product.imageUrl,
180+
product.imageUrl,
181+
]
177182

178183
return (
179184
<div className="min-h-screen bg-stone-50 dark:bg-gray-900">
@@ -235,7 +240,7 @@ export default function ProductDetailPage({
235240
<Star
236241
key={i}
237242
className={`h-5 w-5 ${
238-
i < Math.floor(product.reviewScore ?? 0)
243+
i < Math.floor(product.reviewScore)
239244
? 'fill-current text-amber-500'
240245
: 'text-gray-300 dark:text-gray-600'
241246
}`}
@@ -267,8 +272,8 @@ export default function ProductDetailPage({
267272
<button
268273
key={size}
269274
disabled={!availableSizes.includes(size)}
270-
onClick={handleSizeChange(size)}
271275
title={!availableSizes.includes(size) ? 'Out of stock' : ''}
276+
onClick={handleSizeChange(size)}
272277
className={`rounded-lg border px-4 py-3 text-center transition-colors duration-200 disabled:opacity-20 ${
273278
selectedSize === size
274279
? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
@@ -291,10 +296,10 @@ export default function ProductDetailPage({
291296
<button
292297
key={color}
293298
disabled={!availableColors.includes(color)}
294-
onClick={handleColorChange(color)}
295299
title={
296300
!availableColors.includes(color) ? 'Out of stock' : ''
297301
}
302+
onClick={handleColorChange(color)}
298303
className={`rounded-lg border px-4 py-2 transition-colors duration-200 disabled:opacity-20 ${
299304
selectedColor === color
300305
? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'

0 commit comments

Comments
 (0)