Skip to content

Commit f7ed73c

Browse files
committed
adding remix exercise guides
1 parent 59222a9 commit f7ed73c

File tree

11 files changed

+347
-16
lines changed

11 files changed

+347
-16
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Routes And Layouts
2+
3+
The goal of this exercise is to make files for these routes:
4+
5+
```js
6+
// URL LAYOUTS CONTENT
7+
// localhost:3000 DefaultLayout "Home Page"
8+
// localhost:3000/contact DefaultLayout "Contact"
9+
// localhost:3000/auth/login AuthLayout "Login"
10+
// localhost:3000/auth/register AuthLayout "Register"
11+
// localhost:3000/products ProductsLayout "Browse Products"
12+
// localhost:3000/products/1 ProductsLayout "Product Profile: 1"
13+
// localhost:3000/products/special ProductsLayout "Special Product Profile"
14+
```
15+
16+
## Task 1
17+
18+
We have already provided three "layout" components for you in `templates.tsx`. They are color coded so each type of layout looks different. They are Gray, Green, and Blue. Each component needs to be converted to its own file in accordance with the URL scheme above. For example, if you visit `/products`, you should see a page loaded into the `ProductsLayout` and the page should say `<h1>Browse Products</h1>`. Then you can see there are two other pages that also use that same layout for when you visit `/products/1` and `products/special`. This all means you'll need for files, one for the layout and three for the pages.
19+
20+
By the time you're done, you should have removed the original `_index.tsx` and `templates.tsx` files as they do not conform to the URL scheme, they're just to get you started.
21+
22+
Here's an example "Page" component. This is about what you'll need for most pages
23+
24+
```tsx
25+
// You could name it Page or any name you like
26+
export default function Page() {
27+
return (
28+
<div>
29+
<h1>Browse Products</h1>
30+
</div>
31+
)
32+
}
33+
34+
// Or you could make it an anonymous function
35+
export default function () {
36+
return (
37+
<div>
38+
<h1>Browse Products</h1>
39+
</div>
40+
)
41+
}
42+
```
43+
44+
## Final
45+
46+
Remember, for this and all exercises, there's a `final` version for you to review if you need to with all the answers. In this case, the final is in `exercise-final`

remix/lessons/01-routes-and-layouts/exercise/routes/templates.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import { Outlet } from '@remix-run/react'
22

3-
// localhost:3000 DefaultLayout "Home Page"
4-
// localhost:3000/contact DefaultLayout "Contact"
5-
// localhost:3000/auth/login AuthLayout "Login"
6-
// localhost:3000/auth/register AuthLayout "Register"
7-
// localhost:3000/products ProductsLayout "Browse Products"
8-
// localhost:3000/products/1 ProductsLayout "Product Profile: 1"
9-
// localhost:3000/products/special ProductsLayout "Special Product Profile"
10-
113
// Gray
124
function DefaultLayout() {
135
return (
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Loaders
2+
3+
The goal of this exercise is to write loaders for products
4+
5+
## Task 1
6+
7+
The current setup has a loader in every file. But if you notice, the pages each try to do `getProducts()` on their own and this could be done in the layout instead. The layout already fetches brands so you'll need to run the promise for `getBrands()` and `getProducts` in parallel and return both results
8+
9+
## Task 2
10+
11+
When the layout component is successfully receiving `brands` and `products` from `useLoaderData()`, you'll need to figure out a way to pass this data down into the page. Keep in mind that the `<Outlet />` is not an alias for your page such that you can just pass props into it and these props will end up being props in your component. If you need to pass data from a layout into the page that the Outlet represents, you can do context.
12+
13+
1. Make a context variable and pass it into the outlet: `<Outlet context={context} />`
14+
2. Outlets are technically "Context Providers" so you don't need to make your own context the way we would have done for a traditional React SPA.
15+
3. Get the context value in the page component with `useOutletContext()`
16+
17+
You might see some typescript errors because `useOutletContext()` has no clue what it's receiving. If the code is accurate, it should still work, or you can do `useOutletContext<any>()` to tell TS not to complain.
18+
19+
## Task 3
20+
21+
Context is the only way to "pass" data from a layout to a page, but technically we don't need to do that at all. The loader data from the layout is also available to all the sub pages or layouts so you could just call useRouteLoaderData('route/your-path') in the page instead of `useOutletContext()`.
22+
23+
Switch away from context to `useRouteLoaderData(path)`
24+
25+
Docs: https://remix.run/docs/en/main/hooks/use-route-loader-data

remix/lessons/02-loaders/exercise/routes/_products-layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { UnpackLoader } from '~/utils/helpers'
77

88
export const loader = async ({ request }: LoaderArgs) => {
99
const brands = await getBrands()
10-
return json({ brands })
10+
return json(brands)
1111
}
1212

1313
export type LoaderData = UnpackLoader<typeof loader>
1414

1515
export default function () {
16-
const { brands } = useLoaderData<LoaderData>()
16+
const brands = useLoaderData<LoaderData>()
1717

1818
return (
1919
<div className="flex gap-6">
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { NavLink } from '@remix-run/react'
2+
import { SelectedLesson } from '~/components/LessonContext'
3+
import { Logo } from '~/components/Logo'
4+
5+
type MainLayoutProps = {
6+
children: React.ReactNode
7+
}
8+
9+
export function MainLayout({ children }: MainLayoutProps) {
10+
return (
11+
<div>
12+
<Header />
13+
<SubHeader />
14+
<CenterContent className="pt-6 pb-20">{children}</CenterContent>
15+
</div>
16+
)
17+
}
18+
19+
function Header() {
20+
return (
21+
<header className="d bg-gradient-to-r from-sky-400 to-indigo-950">
22+
<CenterContent className="border-b py-3">
23+
<div className="flex justify-between items-center">
24+
<div className="">
25+
<Logo />
26+
</div>
27+
<div className="text-white/60">
28+
<SelectedLesson />
29+
</div>
30+
</div>
31+
</CenterContent>
32+
</header>
33+
)
34+
}
35+
36+
function SubHeader() {
37+
return (
38+
<CenterContent className="bg-white border-b">
39+
<nav className="primary-nav">
40+
<NavLink className="inline-block py-3 px-5 -mb-[1px] border-b-2" to="/">
41+
Home
42+
</NavLink>
43+
</nav>
44+
</CenterContent>
45+
)
46+
}
47+
48+
type CenterContentProps = {
49+
className?: string
50+
children: React.ReactNode
51+
}
52+
53+
export function CenterContent({ children, className }: CenterContentProps) {
54+
return (
55+
<div className={className}>
56+
<div className="ml-auto mr-auto max-w-[1200px] pl-3 pr-3">{children}</div>
57+
</div>
58+
)
59+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
Links,
3+
LiveReload,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
useLoaderData,
9+
} from '@remix-run/react'
10+
import { type LinksFunction, json } from '@remix-run/node'
11+
import stylesheet from '~/styles/app.css'
12+
import { MainLayout } from './components/MainLayout'
13+
import { LessonProvider } from '~/components/LessonContext'
14+
15+
export const links: LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }]
16+
17+
export async function loader() {
18+
const lesson = process.env.REMIX_APP_DIR?.split('/').slice(-2).join('/') || ''
19+
return json({ lesson })
20+
}
21+
22+
export default function App() {
23+
const { lesson } = useLoaderData<typeof loader>()
24+
25+
return (
26+
<html lang="en">
27+
<head>
28+
<meta charSet="utf-8" />
29+
<meta name="viewport" content="width=device-width,initial-scale=1" />
30+
<Meta />
31+
<Links />
32+
<link rel="preconnect" href="https://fonts.googleapis.com" />
33+
<link
34+
href="https://fonts.googleapis.com/css2?&family=Inter:wght@400;700&display=swap"
35+
rel="stylesheet"
36+
/>
37+
</head>
38+
<body>
39+
<LessonProvider selectedLesson={lesson}>
40+
<MainLayout>
41+
<Outlet />
42+
</MainLayout>
43+
</LessonProvider>
44+
<ScrollRestoration />
45+
<Scripts />
46+
<LiveReload />
47+
</body>
48+
</html>
49+
)
50+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useOutletContext } from '@remix-run/react'
2+
import { type ProductType } from '~/utils/db.server'
3+
import { type CartItemType } from '~/utils/cart.server'
4+
import { Tiles } from '~/components/Tiles'
5+
import { Icon } from '~/components/Icon'
6+
7+
type OutletContext = {
8+
products: ProductType[]
9+
cart: CartItemType[]
10+
}
11+
12+
export default function () {
13+
const { products } = useOutletContext<OutletContext>()
14+
15+
return (
16+
<Tiles>
17+
{products.map((product) => {
18+
return (
19+
<div
20+
key={product.id}
21+
className="p-3 rounded-lg bg-white shadow-sm overflow-hidden flex flex-col"
22+
>
23+
<img
24+
src={`/images/products/${product.image}`}
25+
alt={product.name}
26+
className="block object-contain h-52"
27+
/>
28+
<div className="space-y-3 mt-3 border-t">
29+
<div className="mt-3 flex justify-between items-center">
30+
<div className="">{product.name}</div>
31+
<b className="block">${product.price}</b>
32+
</div>
33+
<div className="flex gap-2">
34+
<div className="flex-1">
35+
<button className="button button-outline whitespace-nowrap" type="submit">
36+
<Icon name="cart" />
37+
</button>
38+
</div>
39+
<div className="w-full flex flex-col">
40+
<button className="button">View</button>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
)
46+
})}
47+
</Tiles>
48+
)
49+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useId, useMemo } from 'react'
2+
import { json } from '@remix-run/node'
3+
import { Link, Outlet, useLoaderData, useLocation, useSearchParams } from '@remix-run/react'
4+
import { Heading } from '~/components/Heading'
5+
import { getBrands, getProducts } from '~/utils/db.server'
6+
import { sortLabel } from '~/utils/helpers'
7+
import { Icon } from '~/components/Icon'
8+
import type { LoaderArgs } from '@remix-run/node'
9+
10+
export const loader = async ({ request }: LoaderArgs) => {
11+
const searchParams = new URL(request.url).searchParams
12+
const [products, brands] = await Promise.all([getProducts(searchParams), getBrands()])
13+
14+
return json({
15+
products,
16+
brands: brands.sort(sortLabel),
17+
})
18+
}
19+
20+
export default function () {
21+
const { products, brands } = useLoaderData<typeof loader>()
22+
const context = useMemo(() => ({ products }), [products])
23+
24+
return (
25+
<div className="flex gap-6">
26+
<aside className="w-72 p-6 rounded-lg bg-white shadow-sm space-y-6">
27+
<section className="space-y-1">
28+
<Heading as="h2" size={4}>
29+
Filter By Brand
30+
</Heading>
31+
{brands.map((brand) => {
32+
return (
33+
<FilterLink key={brand.id} value={brand.handle}>
34+
{brand.label}
35+
</FilterLink>
36+
)
37+
})}
38+
</section>
39+
</aside>
40+
<main className="flex-1 space-y-3">
41+
<Outlet context={context} />
42+
</main>
43+
</div>
44+
)
45+
}
46+
47+
function FilterLink({ children, value }: { children: React.ReactNode; value: string }) {
48+
const id = useId()
49+
const url = useLocation().pathname
50+
51+
// The current URL
52+
const [search] = useSearchParams()
53+
const urlValue = search.get('brand')?.toLowerCase().split(',')
54+
const on = Array.isArray(urlValue) && urlValue.includes(value.toLowerCase())
55+
56+
// The next URL
57+
const nextSearch = new URLSearchParams(search.toString())
58+
const valuesFiltered = Array.isArray(urlValue) ? urlValue.filter((v) => v && v !== value) : []
59+
60+
if (on) {
61+
nextSearch.set('brand', valuesFiltered.join(','))
62+
} else {
63+
nextSearch.set('brand', valuesFiltered.concat(value).join(','))
64+
}
65+
66+
const to = `${url}?${nextSearch.toString()}`
67+
68+
return (
69+
<Link to={to} className="block">
70+
<input id={id} type="checkbox" className="hidden" />
71+
<span className="text-brandColor mr-2">
72+
{on ? <Icon name="checkboxOn" /> : <Icon name="checkboxOff" />}
73+
</span>
74+
<label htmlFor={id} className="cursor-pointer">
75+
{children}
76+
</label>
77+
</Link>
78+
)
79+
}
Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
1-
https://medium.com/swlh/urlsearchparams-in-javascript-df524f705317
1+
# URL State
2+
3+
The goal of this exercise is to practice using the URL for state. The URL is not only a legitimate place to keep state between the front-end and back-end, it's actually the original place for keeping state before Single Page Apps.
4+
5+
# Task 1
6+
7+
The only component you'll need to work on is inside of `_products-layout` called `FilterLink`. Right now, it works for clicking on filters in the UI and toggling the URL so only one brand can be filtered at one time. We want you to re-write the algorithm in `FilterLink` so if the user clicks two checkboxes, we'll have two brands in the filter like this:
8+
9+
localhost:3000/products?brand=apple%20google
10+
11+
Keep in mind that %20 is a URL-encoded comma. When you set the value in your code, you'll set it as `apple,google` and the URLSearchParams class will take care of the encoding for you. When you read the value from the URL, they'll take care of the decoding for you and you'll receive `apple,google`.
12+
13+
1. When a user clicks Apple, the URL should change to `?brand=apple`
14+
2. When the user then clicks Google, the URL should change to `?brand=apple%20google`
15+
3. When the user then clicks Apple again, it will toggle off in the URL and the URL will be `?brand=google`
16+
17+
Tread this algorithm as if you're just working on an array and taking items in and out of an array.
218

319
```js
4-
// Three ways to get the search params object
5-
const url = new URL('https://example.com?a=b')
6-
const search = url.searchParams
20+
// Here are some basic JavaScript functions we used in the solution that you might need:
21+
myString.split(',') // Turn a string into an array, split by commas
22+
myString.toLowerCase() // Lower-cases a string
23+
myArray.join(',') // Turn an array into a string, joined by commas
24+
Array.isArray(myArray) // Returns true if the array is an array
25+
myArray.filter(fn) // Returns a new array based on the filter function. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
26+
myArray.concat(newValue) // Similar to push but this returns a new array with the value added
727

28+
// URLSearchParams is a web standard to do CRUD operations on the URL search params
829
const search = new URLSearchParams('https://example.com?a=b')
930

10-
// Hooks way
31+
// search is an instance of `new URLSearchParams`
1132
const [search] = useSearchParams()
1233

1334
// Getters and setters
@@ -16,4 +37,10 @@ const brand = search.get('brand')
1637

1738
// Delete
1839
search.delete('brand')
40+
41+
// Serialize the search object into a string
42+
search.toString()
1943
```
44+
45+
Additional Help:
46+
https://medium.com/swlh/urlsearchparams-in-javascript-df524f705317

0 commit comments

Comments
 (0)