Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/migration #209

Merged
merged 48 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5c4305e
chore: git mv tailwind, assets, storybook
nlkluth Oct 20, 2023
7c6ed43
wip
nlkluth Oct 23, 2023
f976705
pnpm version mismatch
nlkluth Oct 23, 2023
064a10a
fix: ts version mismatch
nlkluth Oct 23, 2023
a79aadc
chore: basic tsup setup
nlkluth Oct 23, 2023
9c5f13e
ci step
nlkluth Oct 23, 2023
c12cb17
chore: e2e ci
nlkluth Oct 23, 2023
0a5ded3
Merge branch 'main' into feature/ui-lib
nlkluth Oct 24, 2023
27298e0
refactor: ui -> shared-ui
nlkluth Oct 24, 2023
1944f20
chore: shared tsconfig
nlkluth Oct 24, 2023
94dc242
fix: tsconfig include/exclude
nlkluth Oct 24, 2023
ef17231
chore: build for next deploy
nlkluth Oct 24, 2023
a449c81
fix: pull in external classes
nlkluth Oct 24, 2023
0eb08fb
mv Typography
nlkluth Oct 25, 2023
3751513
mv Input
nlkluth Oct 25, 2023
53807e5
Merge branch 'main' into chore/migration
nlkluth Oct 25, 2023
465a599
mv ui folder
nlkluth Oct 25, 2023
c09c225
mv banner
nlkluth Oct 25, 2023
7e17015
fix file path
nlkluth Oct 25, 2023
2a4060b
mv pill
nlkluth Oct 25, 2023
4e12912
mv small components
nlkluth Oct 26, 2023
f760d78
add dep
nlkluth Oct 26, 2023
be8f38b
mv select
nlkluth Oct 26, 2023
c2c43bf
mv modal
nlkluth Oct 26, 2023
0f51823
mv linktext blockcontent
nlkluth Oct 26, 2023
20540e4
mv price
nlkluth Oct 26, 2023
60348fc
mv quantityinput
nlkluth Oct 26, 2023
13e68c3
fix ci package dir
nlkluth Oct 26, 2023
e44fc6c
wip
nlkluth Oct 30, 2023
ec7b493
wip
nlkluth Oct 30, 2023
cbbfbdf
wiring
nlkluth Oct 30, 2023
6b346df
wiring and fixing
nlkluth Oct 30, 2023
038cb19
price
nlkluth Oct 30, 2023
ebc059c
_id
nlkluth Oct 30, 2023
370706b
mv cart lineitem
nlkluth Oct 31, 2023
9fddd8a
mv
nlkluth Oct 31, 2023
0711bf5
typo
nlkluth Oct 31, 2023
bd7393e
typo
nlkluth Oct 31, 2023
fed05dd
cleanup
nlkluth Oct 31, 2023
ba6b7ac
fix render
nlkluth Oct 31, 2023
a54fe2c
initial story
nlkluth Nov 1, 2023
dd1b833
styling and helpers
nlkluth Nov 1, 2023
fc7370d
custom actions
nlkluth Nov 1, 2023
71e7fee
test
nlkluth Nov 1, 2023
a78afbe
tests
nlkluth Nov 1, 2023
63b72e4
Merge branch 'main' into chore/migration
nlkluth Nov 2, 2023
8ee1c18
move totals to reducer
nlkluth Nov 6, 2023
546fb04
fix cart closing
nlkluth Nov 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 18 additions & 89 deletions packages/nextjs/components/CartContext.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,9 @@
import * as React from "react";
import { q } from "groqd";
import { useReducer } from "react";
import { CartItem, CartUpdate, CartProvider as SharedCartProvider } from "shared-ui";
import { runQuery } from "utils/sanityClient";

export type CartItem = {
_id: string;
qty: number;
variantInfo: CartItemVariant;
};

const initialValue = {
isFetchingCartItems: false,
cartItems: [] as CartItem[],
cartItemsErrorIds: [] as string[] | undefined,
cartTotal: 0,
totalCartPrice: 0,
updateCart: (() => null) as (productId: string, quantity: number) => void,
clearCart: (() => null) as () => void,
updateCartFromApi: (() => null) as () => void,
};

type CartContextValue = typeof initialValue;

export const CartContext = React.createContext<CartContextValue>(initialValue);
export const useCart = () => React.useContext(CartContext);

type State = {
cartItems: CartItem[];
cartItemsErrorIds: string[];
fetching: boolean;
};

type Action =
| { type: "loading" }
| { type: "success"; cartItems: CartItem[]; cartItemsErrorIds: string[] }
| { type: "clear" };

const defaultState: State = { cartItems: [], cartItemsErrorIds: [], fetching: false };

const cartReducer = (state: State, action: Action): State => {
switch (action.type) {
case "loading":
return { ...state, cartItemsErrorIds: [], fetching: true };
case "success":
return { ...state, cartItems: action.cartItems, cartItemsErrorIds: action.cartItemsErrorIds, fetching: false };
case "clear":
return defaultState;
}
};

export const CartProvider = ({ children }: React.PropsWithChildren) => {
const [{ cartItems, cartItemsErrorIds, fetching }, dispatch] = useReducer(cartReducer, defaultState);
const retrieveCartItems = React.useCallback(async (cart: Record<string, number>) => {
const cartEntries = Object.entries(cart);

Expand All @@ -72,36 +25,40 @@ export const CartProvider = ({ children }: React.PropsWithChildren) => {
const res = await throttle(getResults, 1000);
const errorRetrievingIds: string[] = [];

const formattedItems = cartEntries.reduce<CartItem[]>((acc, [variantId, quantity]) => {
const results = cartEntries.reduce<CartItem[]>((acc, [variantId, quantity]) => {
const productInfo = res.find((variant) => variant._id === variantId);
if (productInfo) {
return [...acc, { _id: variantId, qty: quantity, variantInfo: productInfo }];
return [
...acc,
{
_id: variantId,
quantity,
name: productInfo.name,
price: productInfo.price,
},
];
}

errorRetrievingIds.push(variantId);
return acc;
}, []);

dispatch({ type: "success", cartItems: formattedItems, cartItemsErrorIds: errorRetrievingIds });
} else {
dispatch({ type: "clear" });
return { results, errors: errorRetrievingIds };
}

return { results: [], errors: [] };
}, []);

const updateCartFromApi = React.useCallback(async () => {
const data = await fetch("/api/cart", {
credentials: "same-origin",
}).then((res) => res.json());

retrieveCartItems(data);
return retrieveCartItems(data);
}, [retrieveCartItems]);

React.useEffect(() => {
updateCartFromApi();
}, [updateCartFromApi]);

const updateCart = React.useCallback(
async (variantId: string, quantity: number) => {
async ({ _id: variantId, quantity }: CartUpdate) => {
if (!variantId) {
return;
}
Expand Down Expand Up @@ -135,47 +92,19 @@ export const CartProvider = ({ children }: React.PropsWithChildren) => {
"Content-Type": "application/json",
},
}).then((res) => res.json());

dispatch({ type: "clear" });
} catch (error) {
console.error(error);
}
};

// Calculates the total quantity of all items in the cart
const cartTotal = React.useMemo(() => cartItems.reduce((acc, { qty }) => acc + qty, 0), [cartItems]);

const totalCartPrice = React.useMemo(
() => cartItems.reduce((acc, { qty, variantInfo }) => acc + qty * variantInfo.price, 0),
[cartItems]
);

return (
<CartContext.Provider
value={{
isFetchingCartItems: fetching,
cartItems,
cartItemsErrorIds,
updateCart,
clearCart,
cartTotal,
updateCartFromApi,
totalCartPrice,
}}
>
<SharedCartProvider onCartClear={clearCart} onCartFetch={updateCartFromApi} onCartUpdate={updateCart}>
{children}
</CartContext.Provider>
</SharedCartProvider>
);
};

const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));

const throttle = <T,>(action: () => Promise<T>, ms: number): Promise<T> =>
Promise.all([action(), wait(ms)]).then((res) => res[0]);

type CartItemVariant = {
_id: string;
name: string;
msrp: number;
price: number;
};
30 changes: 13 additions & 17 deletions packages/nextjs/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as React from "react";
import Link from "next/link";
import classNames from "classnames";
import { useCart } from "components/CartContext";
import { Cart, CartContent, Button, useCart } from "shared-ui";
import { Search } from "components/Search";
import { Cart } from "./Cart";
import { MobileHeaderItems } from "./MobileHeaderItems";
import { NAV_ITEMS } from "./NavItems";
import { MobileNavMenu } from "./MobileNavMenu";
Expand All @@ -12,9 +11,7 @@ import { Logo } from "./Logo";

export const Header = () => {
const [isNavOpen, setIsNavOpen] = React.useState(false);

const [isCartOpen, setIsCartOpen] = React.useState(false);
const { cartTotal, isFetchingCartItems } = useCart();
const { toggleCartOpen } = useCart();

const onMobileNavClick = () => setIsNavOpen((prev) => !prev);
const onMobileNavClose = () => setIsNavOpen(false);
Expand All @@ -23,10 +20,6 @@ export const Header = () => {
document.body.classList[isNavOpen ? "add" : "remove"]("overflow-hidden");
}, [isNavOpen]);

React.useEffect(() => {
document.body.classList[isCartOpen ? "add" : "remove"]("overflow-hidden", "sm:overflow-auto");
}, [isCartOpen]);

return (
<header className={classNames("sticky top-0 z-10 flex flex-col", isNavOpen && "h-screen")}>
<nav className="h-[66px] sm:h-[110px] border-b-2 border-b-primary bg-secondary shadow transition-all text-primary">
Expand All @@ -44,14 +37,17 @@ export const Header = () => {
</div>
<div className="flex items-center">
<Search />
<Cart
cartTotal={cartTotal}
onMobileNavClose={onMobileNavClose}
isFetchingCartItems={isFetchingCartItems}
openCart={() => setIsCartOpen(true)}
isCartOpen={isCartOpen}
onCartClose={() => setIsCartOpen(false)}
/>
<Cart onMobileNavClose={onMobileNavClose}>
<CartContent
ProductListLink={
<Link href="/products" passHref legacyBehavior>
<Button as="a" variant="secondary" onClick={() => toggleCartOpen(false)}>
View Products
</Button>
</Link>
}
/>
</Cart>
<MobileNavMenu navOpen={isNavOpen} onMobileNavClick={onMobileNavClick} />
</div>
</div>
Expand Down
10 changes: 7 additions & 3 deletions packages/nextjs/pages/products/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import { GetServerSideProps, NextPage } from "next";
import { useRouter } from "next/router";
import { AnimatePresence } from "framer-motion";

import { H6, FadeInOut, BlockContent, Price, QuantityInput } from "shared-ui";
import { H6, FadeInOut, BlockContent, Price, QuantityInput, useCart } from "shared-ui";
import { setCachingHeaders } from "utils/setCachingHeaders";
import { isSlug } from "utils/isSlug";
import { SanityType } from "utils/consts";
import { getRecommendations } from "utils/getRecommendationsQuery";
import { getProductBySlug } from "utils/getProductBySlug";

import { ImageCarousel } from "components/ImageCarousel";
import { useCart } from "components/CartContext";
import { PageHead } from "components/PageHead";
import { StyleOptions } from "components/ProductPage/StyleOptions";
import { ProductVariantSelector } from "components/ProductPage/ProductVariantSelector";
Expand Down Expand Up @@ -104,7 +103,12 @@ const PageBody = ({ variant, product }: { product?: ProductType; variant?: Varia
// If the item is already in the cart allow user to click add to cart multiple times
const existingCartItem = cartItems.find((item) => item._id === variant._id);

updateCart(variant?._id, existingCartItem ? existingCartItem.qty + Number(quantity) : Number(quantity));
updateCart({
_id: variant._id,
name: variant.name,
price: variant.price,
quantity: existingCartItem ? existingCartItem.quantity + Number(quantity) : Number(quantity),
});
}
};

Expand Down
7 changes: 6 additions & 1 deletion packages/shared-ui/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
const config = {
stories: ["../**/*.mdx", "../**/*.stories.@(js|jsx|mjs|ts|tsx)"],
stories: ["../**/*.mdx", "../components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
],
docs: {
autodocs: "tag",
},
framework: {
name: "@storybook/react-vite",
options: {},
},
};
export default config;
2 changes: 1 addition & 1 deletion packages/shared-ui/components/Price.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { Eyebrow } from "./Typography";
import { currencyFormatter } from "../uitls/currencyFormatter";
import { currencyFormatter } from "../utils/currencyFormatter";

interface Props {
msrp?: number | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import * as React from "react";
import { FiShoppingCart, FiLoader } from "react-icons/fi";
import { AnimatePresence, motion } from "framer-motion";
import { CartContent } from "./CartContent";
import { useCart } from "./CartContext";

type CartProps = {
onMobileNavClose: () => void;
isFetchingCartItems: boolean;
cartTotal: number;
isCartOpen: boolean;
openCart: () => void;
onCartClose: () => void;
};

export const Cart = ({
cartTotal,
onMobileNavClose,
isFetchingCartItems,
openCart,
isCartOpen,
onCartClose,
}: CartProps) => {
const cartPopup = React.useRef<HTMLDivElement>(null);
export const Cart = ({ onMobileNavClose, children }: React.PropsWithChildren<CartProps>) => {
const { isCartOpen, cartPopupRef, toggleCartOpen, isLoading, totalQuantity } = useCart();

React.useEffect(() => {
if (!isCartOpen) return;
Expand All @@ -30,15 +18,15 @@ export const Cart = ({
const hit = evt.target;
if (!(hit instanceof Node && hit instanceof Element)) return;

if (hit.closest(".cart-popup") !== cartPopup.current) {
onCartClose();
if (hit.closest(".cart-popup") !== cartPopupRef?.current) {
toggleCartOpen(false);
}
};
window.addEventListener("click", handleClick);

// Esc-key
const handleKeyup = (evt: KeyboardEvent) => {
if (evt.key === "Escape") onCartClose();
if (evt.key === "Escape") toggleCartOpen(false);
};
window.addEventListener("keyup", handleKeyup);

Expand All @@ -55,12 +43,13 @@ export const Cart = ({
onClick={(e) => {
e.stopPropagation();
onMobileNavClose();
openCart();
toggleCartOpen(true);
}}
data-testid="cart"
>
<span className="hidden sm:block">Cart</span>
<AnimatePresence mode="popLayout">
{isFetchingCartItems ? (
{isLoading ? (
<motion.div key="loader" exit={{ opacity: 0, scale: 0.4 }}>
<FiLoader size={20} className="motion-safe:animate-[spin_2s_ease-in-out_infinite]" />
</motion.div>
Expand All @@ -73,21 +62,21 @@ export const Cart = ({
transition={{ duration: 0.2, ease: "easeIn" }}
>
<FiShoppingCart size={24} className="mx-2" />
<span>{cartTotal}</span>
<span>{totalQuantity}</span>
</motion.div>
)}
</AnimatePresence>
</button>
<AnimatePresence>
{isCartOpen && (
<motion.div
ref={cartPopup}
ref={cartPopupRef}
className="cart-popup fixed inset-0 sm:absolute bg-secondary sm:top-10 sm:right-0 sm:left-[inherit] sm:bottom-[inherit] sm:w-[400px] sm:max-h-[calc(100vh-100px)] sm:shadow-lg sm:rounded sm:border border-primary flex cursor-auto"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<CartContent onClose={onCartClose} />
{children}
</motion.div>
)}
</AnimatePresence>
Expand Down
Loading
Loading