Skip to content

Commit 393d38e

Browse files
committed
remove ex 5
1 parent 2737f67 commit 393d38e

File tree

105 files changed

+297
-7242
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+297
-7242
lines changed

exercises/03.data-fetching/01.problem.fetching-with-loaders/README.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
## Data fetching with loaders
1+
# Data fetching with loaders
22

33
Amazing work so far! Now is the time for us to dive deep into data fetching using
44
loaders in React Router. Loaders are a powerful feature that allows us to fetch
55
data before rendering a route, ensuring that our components have the data they need
66
right from the start.
77

8-
### Loaders Overview
8+
## Loaders Overview
99

1010
Loaders are functions that run on the server (or during the build process in SPA mode)
1111
before a route is rendered. (Keep this in mind if you want navigations to be instant)
@@ -32,7 +32,7 @@ which can significantly improve the performance of your application by reducing
3232
the number of requests made to your backend services, or improving the response of
3333
the loaders themselves, this is useful for data that doesn't change often.
3434

35-
### Exercise Goals
35+
## Exercise Goals
3636

3737

3838
In this exercise, we will set up loaders for our routes to fetch product data from

exercises/03.data-fetching/01.solution.fetching-with-loaders/README.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Data fetching with loaders
1+
# Data fetching with loaders
22

33
Well done! You have successfully added loaders to multiple routes and you've learned
44
how to optimize data fetching by parallelizing requests using `Promise.all`, how

exercises/03.data-fetching/02.problem.search-with-url/README.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## URL as the source of truth & Progressive enhancement
1+
# URL as the source of truth & Progressive enhancement
22

33
One of the terms tied strongly with React Router is "Progressive Enhancement".
44
Progressive enhancement is the idea of building web applications that work for
@@ -13,7 +13,7 @@ manage application state, we can use the URL to store and retrieve state informa
1313
This approach allows your users to have your website work without JavaScript,
1414
as the URL is always accessible, and servers can render pages based on the URL alone.
1515

16-
### `Form` component
16+
## `Form` component
1717

1818
If you've heard of React Router before, you might be familiar with their `Form` component.
1919
This is a wrapper around the native HTML `<form>` element that enhances its functionality
@@ -32,7 +32,7 @@ You can also change the method to `POST` if you want to submit the form data, th
3232
will submit the data to an `action` function defined on the route instead of the `loader`.
3333

3434

35-
### Exercise Goals
35+
## Exercise Goals
3636

3737

3838
In this exercise, we will implement a search functionality for our product listing

exercises/03.data-fetching/02.solution.search-with-url/README.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## URL as the source of truth & Progressive enhancement
1+
# URL as the source of truth & Progressive enhancement
22

33
Well done! You have successfully
44

exercises/03.data-fetching/03.problem.filtering-and-pagination/README.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Filtering, sorting and pagination
1+
# Filtering, sorting and pagination
22

33
Now that you have a working products listing page, it's time to enhance it with filtering, sorting, and pagination capabilities.
44
In this exercise, you'll implement the following features:
@@ -10,7 +10,7 @@ We will be using the URL as the source of truth and we will learn how to do all
1010
We will rely on the web platform and HTML forms to manage the state of our filters, sorting, and pagination.
1111
We will also use the `useNavigation` hook to provide feedback to the user when the page is loading new data.
1212

13-
### `useNavigation` hook
13+
## `useNavigation` hook
1414

1515
The `useNavigation` hook from React Router provides information about the current navigation state of your application.
1616
This hook is particularly useful for providing feedback to users during navigation events, such as when a page is loading new data.
@@ -27,7 +27,7 @@ For example, you might display a loading spinner when the state is `"loading"` o
2727
This hook can also be used to show optimistic UI by accessing the `formData` property which is filled with the submitted data
2828
during a form submission. This allows you to reflect the changes immediately in the UI before the server responds, enhancing the user experience.
2929

30-
### Exercise Goals
30+
## Exercise Goals
3131

3232
In this exercise, you will enhance the products listing page by implementing filtering, sorting, and pagination features.
3333
You will first use the prepared method for extracting all the search parameters from the URL and then apply those filters to the products listing.

exercises/03.data-fetching/03.solution.filtering-and-pagination/README.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Filtering, sorting and pagination
1+
# Filtering, sorting and pagination
22

33
Well done! You have successfully added filtering, sorting, and pagination to your product listing page.
44
This allows your users to easily find and navigate through products based on their preferences.

exercises/03.data-fetching/04.problem.infinite-fetching-with-fetchers/README.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
## Infinite Scrolling with `useFetcher` hook
1+
# Infinite Scrolling with useFetcher hook
22

33
Now that we have everything in place, the lest step in improving our products listing page is to implement infinite scrolling.
44
This will allow us to load more products as the user scrolls down the page, providing a seamless browsing experience.
55

6-
### `useFetcher` hook
6+
## `useFetcher` hook
77
The `useFetcher` hook from React Router is a powerful tool that allows you to perform data fetching and mutations without causing a full page navigation.
88
When you call `useFetcher`, it returns an object that contains several properties and methods to help you manage data fetching.
99
One of the key properties is `load`, which is a function that you can call to fetch data from a specified URL.
@@ -13,7 +13,7 @@ hook further in upcoming workshops, but for now, it's important to understand th
1313

1414
You typically use this hook when you don't want to add an event to the browsers `history` stack, for example when implementing infinite scrolling.
1515

16-
### Exercise Goals
16+
## Exercise Goals
1717

1818
- Add infinite scrolling to the products listing page using the `useFetcher` hook.
1919
- Use an intersection observer to detect when the user has scrolled to the bottom of the page.

exercises/03.data-fetching/04.problem.infinite-fetching-with-fetchers/app/routes/_landing.products.$productId.tsx

Lines changed: 140 additions & 38 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,
@@ -25,37 +25,79 @@ import { type Route } from './+types/_landing.products.$productId'
2525
export const meta: Route.MetaFunction = ({ matches }) => {
2626
const rootMeta = getMetaFromMatches(matches, 'root')
2727
const prefix = getMetaTitle(rootMeta)
28+
2829
return [
2930
{
3031
title: constructPrefixedTitle('Product overview', prefix),
3132
},
3233
]
3334
}
3435

35-
export const loader = async ({ params }: Route.LoaderArgs) => {
36+
export const loader = async ({ params, request }: Route.LoaderArgs) => {
3637
const { productId } = params
3738
const product = await getProductById(productId)
3839
const relatedProducts = await getRelatedProducts(
3940
productId,
4041
product?.category.id,
4142
product?.brand.id,
4243
)
44+
const uniqueSizes = Array.from(
45+
new Set(product?.variations.map((v) => v.size) || []),
46+
)
47+
const uniqueColors = Array.from(
48+
new Set(product?.variations.map((v) => v.color) || []),
49+
)
50+
51+
const url = new URL(request.url)
52+
const searchParams = url.searchParams
53+
// Get the selected size and color from the search params
54+
const selectedSize = searchParams.get('size') || ''
55+
const selectedColor = searchParams.get('color') || ''
56+
// Determine available sizes for the currently selected color
57+
const availableSizes = selectedColor
58+
? product?.variations
59+
.filter((v) => v.color === selectedColor && v.quantity > 0)
60+
.map((v) => v.size) || []
61+
: uniqueSizes
62+
// Determine available colors for the currently selected size
63+
const availableColors = selectedSize
64+
? product?.variations
65+
.filter((v) => v.size === selectedSize && v.quantity > 0)
66+
.map((v) => v.color) || []
67+
: uniqueColors
68+
69+
const selectedVariation = product?.variations.find(
70+
(v) => v.size === selectedSize && v.color === selectedColor,
71+
)
4372

4473
return {
4574
product,
75+
uniqueSizes,
76+
uniqueColors,
77+
availableSizes,
78+
availableColors,
79+
selectedVariation,
4680
relatedProducts,
4781
}
4882
}
4983

5084
export default function ProductDetailPage({
5185
loaderData,
5286
}: Route.ComponentProps) {
53-
const { product, relatedProducts } = loaderData
54-
const [selectedSize] = useState('')
55-
const [selectedColor] = useState('')
87+
const {
88+
product,
89+
selectedVariation,
90+
uniqueColors,
91+
uniqueSizes,
92+
availableColors,
93+
availableSizes,
94+
relatedProducts,
95+
} = loaderData
96+
const [searchParams, setSearchParams] = useSearchParams()
5697
const [quantity, setQuantity] = useState(1)
5798
const [activeImage, setActiveImage] = useState(0)
58-
99+
const selectedSize = searchParams.get('size') || ''
100+
const selectedColor = searchParams.get('color') || ''
59101
if (!product) {
60102
return (
61103
<div className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-gray-900">
@@ -83,6 +125,53 @@ export default function ProductDetailPage({
83125
alert('Added to cart!')
84126
}
85127

128+
const handleSizeChange = (size: string) => () => {
129+
setSearchParams((prev) => {
130+
const newParams = new URLSearchParams(prev)
131+
if (size === selectedSize) {
132+
newParams.delete('size')
133+
} else {
134+
newParams.set('size', size)
135+
}
136+
const color = newParams.get('color')
137+
// If the selected color is not available for the new size, remove it
138+
const noQuantityForColor =
139+
color &&
140+
product.variations.some(
141+
(v) => v.size === size && v.color === color && v.quantity === 0,
142+
)
143+
144+
if (noQuantityForColor) {
145+
newParams.delete('color')
146+
}
147+
return newParams
148+
})
149+
setQuantity(1)
150+
}
151+
152+
const handleColorChange = (color: string) => () => {
153+
setSearchParams((prev) => {
154+
const newParams = new URLSearchParams(prev)
155+
if (color === selectedColor) {
156+
newParams.delete('color')
157+
} else {
158+
newParams.set('color', color)
159+
}
160+
const size = newParams.get('size')
161+
// If the selected size is not available for the new color, remove it
162+
const noQuantityForSize =
163+
size &&
164+
product.variations.some(
165+
(v) => v.color === color && v.size === size && v.quantity === 0,
166+
)
167+
if (noQuantityForSize) {
168+
newParams.delete('size')
169+
}
170+
return newParams
171+
})
172+
setQuantity(1)
173+
}
174+
86175
// Mock additional images for gallery
87176
const productImages = [
88177
product.imageUrl,
@@ -179,19 +268,21 @@ export default function ProductDetailPage({
179268
Size
180269
</h3>
181270
<div className="grid grid-cols-6 gap-3">
182-
{/* {product.sizes.map((size) => (
271+
{uniqueSizes.map((size) => (
183272
<button
184273
key={size}
185-
onClick={() => setSelectedSize(size)}
186-
className={`rounded-lg border px-4 py-3 text-center transition-colors duration-200 ${
274+
disabled={!availableSizes.includes(size)}
275+
title={!availableSizes.includes(size) ? 'Out of stock' : ''}
276+
onClick={handleSizeChange(size)}
277+
className={`rounded-lg border px-4 py-3 text-center transition-colors duration-200 disabled:opacity-20 ${
187278
selectedSize === size
188279
? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
189280
: 'border-gray-300 text-gray-700 hover:border-amber-300 dark:border-gray-600 dark:text-gray-300 dark:hover:border-amber-700'
190281
}`}
191282
>
192283
{size}
193284
</button>
194-
))} */}
285+
))}
195286
</div>
196287
</div>
197288

@@ -200,53 +291,64 @@ export default function ProductDetailPage({
200291
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
201292
Color
202293
</h3>
203-
<div className="flex space-x-3">
204-
{/* {product.colors.map((color) => (
294+
<div className="flex flex-wrap space-x-3">
295+
{uniqueColors.map((color) => (
205296
<button
206297
key={color}
207-
onClick={() => setSelectedColor(color)}
208-
className={`rounded-lg border px-4 py-2 transition-colors duration-200 ${
298+
disabled={!availableColors.includes(color)}
299+
title={
300+
!availableColors.includes(color) ? 'Out of stock' : ''
301+
}
302+
onClick={handleColorChange(color)}
303+
className={`rounded-lg border px-4 py-2 transition-colors duration-200 disabled:opacity-20 ${
209304
selectedColor === color
210305
? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
211306
: 'border-gray-300 text-gray-700 hover:border-amber-300 dark:border-gray-600 dark:text-gray-300 dark:hover:border-amber-700'
212307
}`}
213308
>
214309
{color}
215310
</button>
216-
))} */}
311+
))}
217312
</div>
218313
</div>
219314

220315
{/* 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>
316+
{selectedVariation ? (
317+
<div>
318+
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
319+
Quantity
320+
</h3>
321+
<div className="flex items-center space-x-4">
322+
<div className="flex items-center rounded-lg border border-gray-300 dark:border-gray-600">
323+
<button
324+
onClick={() => setQuantity(Math.max(1, quantity - 1))}
325+
disabled={quantity === 1}
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+
<Minus className="h-4 w-4" />
329+
</button>
330+
<span className="px-4 py-2 font-medium text-gray-900 dark:text-white">
331+
{quantity}
332+
</span>
333+
<button
334+
onClick={() => setQuantity(quantity + 1)}
335+
disabled={quantity === selectedVariation.quantity}
336+
className="p-2 transition-colors duration-200 hover:bg-gray-100 disabled:opacity-20 disabled:hover:bg-transparent dark:hover:bg-gray-700"
337+
>
338+
<Plus className="h-4 w-4" />
339+
</button>
340+
</div>
242341
</div>
243342
</div>
244-
</div>
343+
) : null}
245344

246345
{/* Add to Cart */}
247346
<div className="flex space-x-4">
248347
<button
249348
onClick={handleAddToCart}
349+
disabled={
350+
!selectedVariation || selectedVariation.quantity === 0
351+
}
250352
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"
251353
>
252354
Add to Cart

exercises/03.data-fetching/04.solution.infinite-fetching-with-fetchers/README.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Infinite Scrolling with `useFetcher` hook
1+
# Infinite Scrolling with useFetcher hook
22

33
One more exercise to go! You have done an amazing job so far.
44

@@ -9,4 +9,4 @@ to detect when the user has scrolled to the bottom of the page.
99
Now all that is left to do for us it to fetch the data on the singular product page using loaders and we're done!
1010

1111

12-
You're doing great! 🚀
12+
You're doing great! 🚀

0 commit comments

Comments
 (0)