Skip to content

Commit 2c81db2

Browse files
committed
modify advanced component design lessons
1 parent 54fd925 commit 2c81db2

38 files changed

+387
-58
lines changed

Diff for: react/_full-app/index.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,10 @@ svg {
174174
padding: 0.5rem 1rem;
175175
}
176176

177-
.button:hover,
177+
/* .button:hover,
178178
.button:focus {
179179
background-color: theme(colors.brandBlueDark);
180-
}
180+
} */
181181

182182
.button.button-outline {
183183
border-color: currentColor;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useState } from 'react'
2+
import { DialogConfirm } from './Dialog.final'
3+
4+
export function App() {
5+
const [isOpen, setIsOpen] = useState(false)
6+
return (
7+
<div>
8+
<button className="button" onClick={() => setIsOpen(true)}>
9+
Open dialog
10+
</button>
11+
12+
<DialogConfirm
13+
title="Remove User"
14+
isOpen={isOpen}
15+
onConfirm={() => {
16+
setIsOpen(false)
17+
// other stuff
18+
}}
19+
onCancel={() => setIsOpen(false)}
20+
>
21+
Are you sure you want to deactivate your account? All of your data will be permanently
22+
removed.
23+
</DialogConfirm>
24+
</div>
25+
)
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
2+
import { useState } from 'react'
3+
4+
export function App() {
5+
const [isOpen, setIsOpen] = useState(false)
6+
return (
7+
<div>
8+
<button className="button" onClick={() => setIsOpen(true)}>
9+
Open dialog
10+
</button>
11+
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
12+
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/20">
13+
<DialogPanel className="max-w-lg space-y-4 bg-white p-12 rounded">
14+
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
15+
<p>
16+
Are you sure you want to deactivate your account? All of your data will be permanently
17+
removed.
18+
</p>
19+
<div className="flex gap-4">
20+
<button className="button" onClick={() => setIsOpen(false)}>
21+
Yes
22+
</button>
23+
<button className="button" onClick={() => setIsOpen(false)}>
24+
Cancel
25+
</button>
26+
</div>
27+
</DialogPanel>
28+
</div>
29+
</Dialog>
30+
</div>
31+
)
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Dialog as HeadlessDialog, DialogPanel, DialogTitle } from '@headlessui/react'
2+
3+
type DialogProps = {
4+
open: boolean
5+
onClose(): void
6+
children: React.ReactNode
7+
}
8+
9+
export function Dialog({ children, open, onClose }: DialogProps) {
10+
return (
11+
<HeadlessDialog open={open} onClose={onClose} className="relative z-50">
12+
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/20">
13+
<DialogPanel className="max-w-lg space-y-4 bg-white p-12 rounded">{children}</DialogPanel>
14+
</div>
15+
</HeadlessDialog>
16+
)
17+
}
18+
19+
type DialogConfirmProps = {
20+
title: string
21+
children: string
22+
onConfirm: () => void
23+
onCancel: () => void
24+
isOpen: boolean
25+
}
26+
27+
export function DialogConfirm({
28+
title,
29+
children,
30+
onConfirm,
31+
onCancel,
32+
isOpen,
33+
}: DialogConfirmProps) {
34+
return (
35+
<Dialog open={isOpen} onClose={onCancel}>
36+
<DialogTitle className="font-bold">{title}</DialogTitle>
37+
<p>{children}</p>
38+
<div className="flex gap-4">
39+
<button className="button" onClick={onConfirm}>
40+
Yes
41+
</button>
42+
<button className="button" onClick={onCancel}>
43+
Cancel
44+
</button>
45+
</div>
46+
</Dialog>
47+
)
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
2+
3+
export function Dialog() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as ReactDOM from 'react-dom/client'
2+
import { App } from './App'
3+
import { LessonBody, LessonCard } from '~/Lesson'
4+
5+
ReactDOM.createRoot(document.getElementById('root')!).render(
6+
<LessonBody>
7+
<LessonCard className="w-96">
8+
<App />
9+
</LessonCard>
10+
</LessonBody>
11+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Heading } from '~/Heading'
2+
import { CartProvider, useCart } from './Cart'
3+
import classnames from 'classnames'
4+
import { DialogConfirm } from './Dialog'
5+
import { useState } from 'react'
6+
7+
export function App() {
8+
return (
9+
<CartProvider>
10+
<ProductDetails productId={1} />
11+
</CartProvider>
12+
)
13+
}
14+
15+
/****************************************
16+
Start Here:
17+
*****************************************/
18+
19+
type Props = {
20+
productId: number
21+
}
22+
23+
function ProductDetails({ productId }: Props) {
24+
return (
25+
<div className="space-y-3">
26+
<Heading>iPhone Pro Max</Heading>
27+
<div>Price: 1,199.00</div>
28+
<AddToCartButton productId={productId} />
29+
</div>
30+
)
31+
}
32+
33+
/****************************************
34+
Specialization (Task Two) Here:
35+
*****************************************/
36+
37+
function AddToCartButton({ productId }: { productId: number }) {
38+
const { cart, addToCart, removeFromCart } = useCart()
39+
const [confirmOpen, setConfirmOpen] = useState(false)
40+
const inCart = cart.includes(productId)
41+
42+
function onClick() {
43+
if (!inCart) {
44+
addToCart(productId)
45+
} else {
46+
setConfirmOpen(true)
47+
}
48+
}
49+
50+
function remove() {
51+
removeFromCart(productId)
52+
setConfirmOpen(false)
53+
}
54+
55+
return (
56+
<>
57+
<button className={classnames('button', { 'bg-red-600': inCart })} onClick={onClick}>
58+
{!inCart ? 'Add To Cart' : 'Remove From Cart'}
59+
</button>
60+
<DialogConfirm
61+
title="Remove from Cart"
62+
onConfirm={remove}
63+
onCancel={() => setConfirmOpen(false)}
64+
isOpen={confirmOpen}
65+
>
66+
Are you sure you want to remove this item from the cart?
67+
</DialogConfirm>
68+
</>
69+
)
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Heading } from '~/Heading'
2+
import { CartProvider, useCart } from './Cart'
3+
import classnames from 'classnames'
4+
import { DialogConfirm } from './Dialog'
5+
import { useState } from 'react'
6+
7+
export function App() {
8+
return (
9+
<CartProvider>
10+
<ProductDetails productId={1} />
11+
</CartProvider>
12+
)
13+
}
14+
15+
/****************************************
16+
Start Here:
17+
*****************************************/
18+
19+
type Props = {
20+
productId: number
21+
}
22+
23+
function ProductDetails({ productId }: Props) {
24+
const { cart, addToCart, removeFromCart } = useCart()
25+
const inCart = cart.includes(productId)
26+
27+
function onClick() {
28+
if (!inCart) {
29+
addToCart(productId)
30+
} else {
31+
removeFromCart(productId)
32+
}
33+
}
34+
35+
return (
36+
<div className="space-y-3">
37+
<Heading>iPhone Pro Max</Heading>
38+
<div>Price: 1,199.00</div>
39+
<button className={classnames('button', { 'bg-red-600': inCart })} onClick={onClick}>
40+
{!inCart ? 'Add To Cart' : 'Remove From Cart'}
41+
</button>
42+
{/* <DialogConfirm
43+
title="Remove from Cart"
44+
onConfirm={() => {
45+
// Your code here
46+
}}
47+
onCancel={() => {
48+
// Your code here
49+
}}
50+
isOpen={false}
51+
>
52+
Are you sure you want to remove this item from the cart?
53+
</DialogConfirm> */}
54+
</div>
55+
)
56+
}
57+
58+
/****************************************
59+
Specialization (Task Two) Here:
60+
*****************************************/
61+
62+
function AddToCartButton({ productId }: { productId: number }) {
63+
//
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createContext, use, useState } from 'react'
2+
3+
/****************************************
4+
YOU DON'T NEED TO TOUCH THIS FILE
5+
*****************************************/
6+
7+
type ContextType = {
8+
cart: number[]
9+
addToCart(id: number): void
10+
removeFromCart(id: number): void
11+
}
12+
13+
const Context = createContext<ContextType>(null!)
14+
15+
export function CartProvider({ children }: { children: React.ReactNode }) {
16+
const [cart, setCart] = useState<number[]>([])
17+
18+
function addToCart(id: number) {
19+
if (!cart.includes(id)) {
20+
setCart(cart.concat(id))
21+
}
22+
}
23+
24+
function removeFromCart(id: number) {
25+
setCart(cart.filter((cartId) => cartId !== id))
26+
}
27+
28+
const context = {
29+
cart,
30+
addToCart,
31+
removeFromCart,
32+
}
33+
34+
return <Context value={context}>{children}</Context>
35+
}
36+
37+
export function useCart() {
38+
const context = use(Context)
39+
if (!context) {
40+
console.error('You are trying to consume Cart context without a provider')
41+
}
42+
return context || {}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Dialog as HeadlessDialog, DialogPanel, DialogTitle } from '@headlessui/react'
2+
3+
/****************************************
4+
YOU DON'T NEED TO TOUCH THIS FILE
5+
*****************************************/
6+
7+
type DialogProps = {
8+
open: boolean
9+
onClose(): void
10+
children: React.ReactNode
11+
}
12+
13+
export function Dialog({ children, open, onClose }: DialogProps) {
14+
return (
15+
<HeadlessDialog open={open} onClose={onClose} className="relative z-50">
16+
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/20">
17+
<DialogPanel className="max-w-lg space-y-4 bg-white p-12 rounded">{children}</DialogPanel>
18+
</div>
19+
</HeadlessDialog>
20+
)
21+
}
22+
23+
type DialogConfirmProps = {
24+
title: string
25+
children: string
26+
onConfirm: () => void
27+
onCancel: () => void
28+
isOpen: boolean
29+
}
30+
31+
export function DialogConfirm({
32+
title,
33+
children,
34+
onConfirm,
35+
onCancel,
36+
isOpen,
37+
}: DialogConfirmProps) {
38+
return (
39+
<Dialog open={isOpen} onClose={onCancel}>
40+
<DialogTitle className="font-bold">{title}</DialogTitle>
41+
<p>{children}</p>
42+
<div className="flex gap-4">
43+
<button className="button" onClick={onConfirm}>
44+
Yes
45+
</button>
46+
<button className="button" onClick={onCancel}>
47+
Cancel
48+
</button>
49+
</div>
50+
</Dialog>
51+
)
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Specialization
2+
3+
## Goals
4+
5+
Implement a confirmation step on the `ProductDetails` component when the user removes an item from the shopping cart. Then make the logic for add/remove from cart with the confirmation more re-usable with specialization.
6+
7+
## Task 1
8+
9+
The only file you'll need to work on is `App.tsx`. Currently there is a working shopping cart button for a product. Implement the `DialogConfirm` component (template code provided in JSX already) so that it asks the user to confirm when they want to remove the item from the cart.
10+
11+
## Task 2
12+
13+
The amount of logic in the `ProductDetails` component to make the button work would have to be repeated in other places where we want another shopping cart button. Let's make a special `AddToCartButton` component that specializes in handling all this logic so the end result is the profile looking like this with an easy-to-use `AddToCartButton` button:
14+
15+
```tsx
16+
function ProductDetails({ productId }: Props) {
17+
return (
18+
<div className="space-y-3">
19+
<Heading>iPhone Pro Max</Heading>
20+
<div>Price: 1,199.00</div>
21+
<AddToCartButton productId={productId} />
22+
</div>
23+
)
24+
}
25+
```

0 commit comments

Comments
 (0)