Skip to content

Commit

Permalink
modify advanced component design lessons
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwestfall committed Dec 11, 2024
1 parent 54fd925 commit 2c81db2
Show file tree
Hide file tree
Showing 38 changed files with 387 additions and 58 deletions.
4 changes: 2 additions & 2 deletions react/_full-app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,10 @@ svg {
padding: 0.5rem 1rem;
}

.button:hover,
/* .button:hover,
.button:focus {
background-color: theme(colors.brandBlueDark);
}
} */

.button.button-outline {
border-color: currentColor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState } from 'react'
import { DialogConfirm } from './Dialog.final'

export function App() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button className="button" onClick={() => setIsOpen(true)}>
Open dialog
</button>

<DialogConfirm
title="Remove User"
isOpen={isOpen}
onConfirm={() => {
setIsOpen(false)
// other stuff
}}
onCancel={() => setIsOpen(false)}
>
Are you sure you want to deactivate your account? All of your data will be permanently
removed.
</DialogConfirm>
</div>
)
}
32 changes: 32 additions & 0 deletions react/advanced-component-design/02-specialization/lecture/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'

export function App() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button className="button" onClick={() => setIsOpen(true)}>
Open dialog
</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/20">
<DialogPanel className="max-w-lg space-y-4 bg-white p-12 rounded">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<p>
Are you sure you want to deactivate your account? All of your data will be permanently
removed.
</p>
<div className="flex gap-4">
<button className="button" onClick={() => setIsOpen(false)}>
Yes
</button>
<button className="button" onClick={() => setIsOpen(false)}>
Cancel
</button>
</div>
</DialogPanel>
</div>
</Dialog>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Dialog as HeadlessDialog, DialogPanel, DialogTitle } from '@headlessui/react'

type DialogProps = {
open: boolean
onClose(): void
children: React.ReactNode
}

export function Dialog({ children, open, onClose }: DialogProps) {
return (
<HeadlessDialog open={open} onClose={onClose} className="relative z-50">
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/20">
<DialogPanel className="max-w-lg space-y-4 bg-white p-12 rounded">{children}</DialogPanel>
</div>
</HeadlessDialog>
)
}

type DialogConfirmProps = {
title: string
children: string
onConfirm: () => void
onCancel: () => void
isOpen: boolean
}

export function DialogConfirm({
title,
children,
onConfirm,
onCancel,
isOpen,
}: DialogConfirmProps) {
return (
<Dialog open={isOpen} onClose={onCancel}>
<DialogTitle className="font-bold">{title}</DialogTitle>
<p>{children}</p>
<div className="flex gap-4">
<button className="button" onClick={onConfirm}>
Yes
</button>
<button className="button" onClick={onCancel}>
Cancel
</button>
</div>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'

export function Dialog() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as ReactDOM from 'react-dom/client'
import { App } from './App'
import { LessonBody, LessonCard } from '~/Lesson'

ReactDOM.createRoot(document.getElementById('root')!).render(
<LessonBody>
<LessonCard className="w-96">
<App />
</LessonCard>
</LessonBody>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Heading } from '~/Heading'
import { CartProvider, useCart } from './Cart'
import classnames from 'classnames'
import { DialogConfirm } from './Dialog'
import { useState } from 'react'

export function App() {
return (
<CartProvider>
<ProductDetails productId={1} />
</CartProvider>
)
}

/****************************************
Start Here:
*****************************************/

type Props = {
productId: number
}

function ProductDetails({ productId }: Props) {
return (
<div className="space-y-3">
<Heading>iPhone Pro Max</Heading>
<div>Price: 1,199.00</div>
<AddToCartButton productId={productId} />
</div>
)
}

/****************************************
Specialization (Task Two) Here:
*****************************************/

function AddToCartButton({ productId }: { productId: number }) {
const { cart, addToCart, removeFromCart } = useCart()
const [confirmOpen, setConfirmOpen] = useState(false)
const inCart = cart.includes(productId)

function onClick() {
if (!inCart) {
addToCart(productId)
} else {
setConfirmOpen(true)
}
}

function remove() {
removeFromCart(productId)
setConfirmOpen(false)
}

return (
<>
<button className={classnames('button', { 'bg-red-600': inCart })} onClick={onClick}>
{!inCart ? 'Add To Cart' : 'Remove From Cart'}
</button>
<DialogConfirm
title="Remove from Cart"
onConfirm={remove}
onCancel={() => setConfirmOpen(false)}
isOpen={confirmOpen}
>
Are you sure you want to remove this item from the cart?
</DialogConfirm>
</>
)
}
64 changes: 64 additions & 0 deletions react/advanced-component-design/02-specialization/practice/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Heading } from '~/Heading'
import { CartProvider, useCart } from './Cart'
import classnames from 'classnames'
import { DialogConfirm } from './Dialog'
import { useState } from 'react'

export function App() {
return (
<CartProvider>
<ProductDetails productId={1} />
</CartProvider>
)
}

/****************************************
Start Here:
*****************************************/

type Props = {
productId: number
}

function ProductDetails({ productId }: Props) {
const { cart, addToCart, removeFromCart } = useCart()
const inCart = cart.includes(productId)

function onClick() {
if (!inCart) {
addToCart(productId)
} else {
removeFromCart(productId)
}
}

return (
<div className="space-y-3">
<Heading>iPhone Pro Max</Heading>
<div>Price: 1,199.00</div>
<button className={classnames('button', { 'bg-red-600': inCart })} onClick={onClick}>
{!inCart ? 'Add To Cart' : 'Remove From Cart'}
</button>
{/* <DialogConfirm
title="Remove from Cart"
onConfirm={() => {
// Your code here
}}
onCancel={() => {
// Your code here
}}
isOpen={false}
>
Are you sure you want to remove this item from the cart?
</DialogConfirm> */}
</div>
)
}

/****************************************
Specialization (Task Two) Here:
*****************************************/

function AddToCartButton({ productId }: { productId: number }) {
//
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createContext, use, useState } from 'react'

/****************************************
YOU DON'T NEED TO TOUCH THIS FILE
*****************************************/

type ContextType = {
cart: number[]
addToCart(id: number): void
removeFromCart(id: number): void
}

const Context = createContext<ContextType>(null!)

export function CartProvider({ children }: { children: React.ReactNode }) {
const [cart, setCart] = useState<number[]>([])

function addToCart(id: number) {
if (!cart.includes(id)) {
setCart(cart.concat(id))
}
}

function removeFromCart(id: number) {
setCart(cart.filter((cartId) => cartId !== id))
}

const context = {
cart,
addToCart,
removeFromCart,
}

return <Context value={context}>{children}</Context>
}

export function useCart() {
const context = use(Context)
if (!context) {
console.error('You are trying to consume Cart context without a provider')
}
return context || {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Dialog as HeadlessDialog, DialogPanel, DialogTitle } from '@headlessui/react'

/****************************************
YOU DON'T NEED TO TOUCH THIS FILE
*****************************************/

type DialogProps = {
open: boolean
onClose(): void
children: React.ReactNode
}

export function Dialog({ children, open, onClose }: DialogProps) {
return (
<HeadlessDialog open={open} onClose={onClose} className="relative z-50">
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/20">
<DialogPanel className="max-w-lg space-y-4 bg-white p-12 rounded">{children}</DialogPanel>
</div>
</HeadlessDialog>
)
}

type DialogConfirmProps = {
title: string
children: string
onConfirm: () => void
onCancel: () => void
isOpen: boolean
}

export function DialogConfirm({
title,
children,
onConfirm,
onCancel,
isOpen,
}: DialogConfirmProps) {
return (
<Dialog open={isOpen} onClose={onCancel}>
<DialogTitle className="font-bold">{title}</DialogTitle>
<p>{children}</p>
<div className="flex gap-4">
<button className="button" onClick={onConfirm}>
Yes
</button>
<button className="button" onClick={onCancel}>
Cancel
</button>
</div>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Specialization

## Goals

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.

## Task 1

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.

## Task 2

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:

```tsx
function ProductDetails({ productId }: Props) {
return (
<div className="space-y-3">
<Heading>iPhone Pro Max</Heading>
<div>Price: 1,199.00</div>
<AddToCartButton productId={productId} />
</div>
)
}
```
Loading

0 comments on commit 2c81db2

Please sign in to comment.