Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Created August 14, 2022 22:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebinsua/f7c383fe5d21240963b681026c64ef4a to your computer and use it in GitHub Desktop.
Save sebinsua/f7c383fe5d21240963b681026c64ef4a to your computer and use it in GitHub Desktop.
import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState
} from "react";
import { createPortal } from "react-dom";
import "./styles.css";
function fetchItems() {
return Promise.resolve([
{ id: 1, name: "Product 1", price: "£9.99" },
{ id: 2, name: "Product 2", price: "£19.99" },
{ id: 3, name: "Product 3", price: "£39.99" },
{ id: 4, name: "Product 4", price: "£199.99" },
{ id: 5, name: "Product 5", price: "£399.99" },
{ id: 6, name: "Product 6", price: "£899.99" },
{ id: 7, name: "Product 7", price: "£1099.99" }
]);
}
function shoppingCartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM": {
return {
...state,
[action.payload]: {
count: state[action.payload]?.count
? state[action.payload].count + 1
: 1
}
};
}
case "REMOVE_ITEM": {
return Object.fromEntries(
Object.entries(state)
.map(([itemId, metadata]) => {
if (action.payload !== itemId) {
return [itemId, metadata];
}
return metadata.count - 1 === 0
? []
: [itemId, { ...metadata, count: metadata.count - 1 }];
})
.filter((item) => item.length !== 0)
);
}
default: {
return state;
}
}
}
function countShoppingCartItems(shoppingCart) {
return Object.values(shoppingCart).reduce(
(acc, metadata) => acc + metadata.count,
0
);
}
const priceToFloat = (price) => parseFloat(price.replaceAll(/[^\d.]*/g, ""));
function ShoppingCartModal({ items = [], shoppingCart = {} }) {
const totalPrice = useMemo(() => {
const itemIdToPrice = new Map(
items.map((item) => [item.id, priceToFloat(item.price)])
);
return Object.entries(shoppingCart).reduce(
(acc, [itemId, metadata]) =>
acc + metadata.count * itemIdToPrice.get(parseInt(itemId, 10)),
0
);
}, [items, shoppingCart]);
const totalCount = useMemo(() => countShoppingCartItems(shoppingCart), [
shoppingCart
]);
return (
<div className="shopping-cart-modal">
<div>
{items.map((item) => (
<>
{item.id in shoppingCart ? (
<div key={item.id}>
{item.name} ({shoppingCart[item.id].count}x) £
{(
shoppingCart[item.id].count * priceToFloat(item.price)
).toFixed(2)}
</div>
) : null}
</>
))}
</div>
<hr />
<div>Total count: {totalCount}</div>
<div>Total price: £{totalPrice.toFixed(2)}</div>
</div>
);
}
const noop = () => undefined;
function Portal({ children, onClose = noop }) {
const ref = useRef(null);
const [container, setContainer] = useState(null);
useEffect(() => {
const ownerDocument = ref.current ? ref.current.ownerDocument : null;
const body = ownerDocument ? ownerDocument.body : null;
const div = ownerDocument ? ownerDocument.createElement("div") : null;
if (body && div) {
div.classList.add("portal");
div.addEventListener("click", () => {
onClose();
});
body.appendChild(div);
setContainer(div);
}
return () => {
if (body && div) {
body.removeChild(div);
}
setContainer(null);
};
}, [onClose]);
return (
<>
<div ref={ref} />
{container ? createPortal(children, container) : null}
</>
);
}
export default function App() {
const [items, setItems] = useState([]);
const [showPortal, setShowPortal] = useState(false);
const [shoppingCart, dispatch] = useReducer(shoppingCartReducer, {});
const totalCount = useMemo(() => countShoppingCartItems(shoppingCart), [
shoppingCart
]);
useEffect(() => {
let ignoreLoad = false;
async function loadItems() {
const newItems = await fetchItems();
if (ignoreLoad) {
return;
}
setItems(newItems);
}
loadItems();
return () => {
ignoreLoad = true;
};
}, []);
const handleRemove = useCallback(function handleRemove(event) {
const itemId = event.currentTarget.dataset["itemId"];
dispatch({ type: "REMOVE_ITEM", payload: itemId });
}, []);
const handleAdd = useCallback(function handleAdd(event) {
const itemId = event.currentTarget.dataset["itemId"];
dispatch({ type: "ADD_ITEM", payload: itemId });
}, []);
return (
<>
{showPortal ? (
<Portal onClose={() => setShowPortal(false)}>
<ShoppingCartModal
items={items}
shoppingCart={shoppingCart}
onClose={() => setShowPortal(false)}
/>
</Portal>
) : null}
<div>
<div className="header">
<button className="basket" onClick={() => setShowPortal((t) => !t)}>
<span role="img" aria-label="Basket">
🧺{totalCount ? ` (${totalCount})` : ""}
</span>
</button>
</div>
<div className="rows">
{items.map((item) => (
<div key={item.id} className="row">
<div>{item.name}</div>
<div className="actions">
<div className="action__add">
{item.id && shoppingCart[item.id] ? (
<span>{shoppingCart[item.id].count}</span>
) : (
""
)}
<button
onClick={handleRemove}
data-item-id={item.id}
disabled={
!(item.id in shoppingCart) ||
(item.id &&
shoppingCart[item.id] &&
shoppingCart[item.id].count === 0)
}
>
-
</button>
<button onClick={handleAdd} data-item-id={item.id}>
+
</button>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment