Skip to content

Commit

Permalink
Merge pull request #67 from buildheadless/cart
Browse files Browse the repository at this point in the history
Add cart drawer
  • Loading branch information
thomasKn authored Jan 5, 2024
2 parents 3ab9730 + 5a428c9 commit 64e98c2
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 27 deletions.
113 changes: 113 additions & 0 deletions templates/hydrogen-theme/app/components/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {Dialog, Transition} from '@headlessui/react';
import {cx} from 'class-variance-authority';
import {Fragment, useState} from 'react';

import {IconClose} from './icons/IconClose';

/**
* Drawer component that opens on user click.
* @param heading - string. Shown at the top of the drawer.
* @param open - boolean state. if true opens the drawer.
* @param onClose - function should set the open state.
* @param openFrom - right, left
* @param children - react children node.
*/
export function Drawer({
children,
heading,
onClose,
open,
openFrom = 'right',
}: {
children: React.ReactNode;
heading?: string;
onClose: () => void;
open: boolean;
openFrom: 'left' | 'right';
}) {
const offScreen = {
left: '-translate-x-full',
right: 'translate-x-full',
};

return (
<Transition appear as={Fragment} show={open}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 left-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-slate-400/50 backdrop-blur-sm transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
<div
className={cx([
'fixed inset-y-0 flex max-w-full',
openFrom === 'right' ? 'right-0' : '',
])}
>
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-300"
enterFrom={offScreen[openFrom]}
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-300"
leaveFrom="translate-x-0"
leaveTo={offScreen[openFrom]}
>
<div className="max-h-screen w-screen max-w-lg bg-white">
<Dialog.Panel className="flex max-h-screen min-h-full flex-col">
<header className="flex items-start justify-between p-5 shadow-sm">
{heading !== null && (
<Dialog.Title className="text-lg">
{heading}
</Dialog.Title>
)}
<button
className="-m-4 p-4 text-primary transition hover:text-primary/50"
data-test="close-cart"
onClick={onClose}
type="button"
>
<IconClose aria-label="Close panel" />
</button>
</header>
{children}
</Dialog.Panel>
</div>
</Transition.Child>
</div>
</div>
</div>
</Dialog>
</Transition>
);
}

/* Use for associating arialabelledby with the title*/
Drawer.Title = Dialog.Title;

export function useDrawer(openDefault = false) {
const [isOpen, setIsOpen] = useState(openDefault);

function openDrawer() {
setIsOpen(true);
}

function closeDrawer() {
setIsOpen(false);
}

return {
closeDrawer,
isOpen,
openDrawer,
};
}
2 changes: 1 addition & 1 deletion templates/hydrogen-theme/app/components/cart/Cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function Cart({
layout,
onClose,
}: {
cart: CartType | null;
cart?: CartType | null;
layout: CartLayouts;
onClose?: () => void;
}) {
Expand Down
27 changes: 18 additions & 9 deletions templates/hydrogen-theme/app/components/cart/CartDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,36 @@ export function CartDetails({
cart,
layout,
}: {
cart: CartType | null;
cart?: CartType | null;
layout: CartLayouts;
}) {
// @todo: get optimistic cart cost
const cartHasItems = !!cart && cart.totalQuantity > 0;
const container = {
drawer: cx('grid grid-cols-1 grid-rows-[1fr_auto]'),
page: cx(
'w-full pb-12 grid md:grid-cols-2 md:items-start gap-8 md:gap-8 lg:gap-12',
),
};

return (
<div className={cx(['container', container[layout]])}>
<CartLines layout={layout} lines={cart?.lines} />
<CartDetailsLayout layout={layout}>
<div className={cx([layout === 'drawer' && 'flex-1 overflow-y-scroll'])}>
<CartLines layout={layout} lines={cart?.lines} />
</div>
{cartHasItems && (
<CartSummary cost={cart.cost} layout={layout}>
<CartDiscounts discountCodes={cart.discountCodes} />
<CartCheckoutActions checkoutUrl={cart.checkoutUrl} />
</CartSummary>
)}
</CartDetailsLayout>
);
}

function CartDetailsLayout(props: {
children: React.ReactNode;
layout: CartLayouts;
}) {
return props.layout === 'drawer' ? (
<>{props.children}</>
) : (
<div className="container grid w-full gap-8 pb-12 md:grid-cols-2 md:items-start md:gap-8 lg:gap-12">
{props.children}
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion templates/hydrogen-theme/app/components/cart/CartEmpty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function CartEmpty({
}) {
const container = {
drawer: cx([
'content-start gap-4 px-6 pb-8 transition overflow-y-scroll md:gap-12 md:px-12md:pb-12',
'p-5 content-start gap-4 pb-8 transition flex-1 overflow-y-scroll md:gap-12 md:pb-12',
]),
page: cx([
!hidden && 'grid',
Expand Down
2 changes: 1 addition & 1 deletion templates/hydrogen-theme/app/components/cart/CartLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function CartLines({
const className = cx([
layout === 'page'
? 'flex-grow md:translate-y-4'
: 'px-6 pb-6 sm-max:pt-2 overflow-auto transition md:px-12',
: 'px-6 py-6 overflow-auto transition md:px-12',
]);

return (
Expand Down
25 changes: 25 additions & 0 deletions templates/hydrogen-theme/app/components/icons/IconClose.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {IconProps} from './Icon';

import {Icon} from './Icon';

export function IconClose(props: IconProps) {
return (
<Icon {...props} stroke={props.stroke || 'currentColor'}>
<title>Close</title>
<line
strokeWidth="1.25"
x1="4.44194"
x2="15.7556"
y1="4.30806"
y2="15.6218"
/>
<line
strokeWidth="1.25"
transform="matrix(-0.707107 0.707107 0.707107 0.707107 16 4.75)"
x2="16"
y1="-0.625"
y2="-0.625"
/>
</Icon>
);
}
26 changes: 16 additions & 10 deletions templates/hydrogen-theme/app/components/layout/CartCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ import {useRootLoaderData} from '~/hooks/useRootLoaderData';

import {IconBag} from '../icons/IconBag';

export function CartCount() {
export function CartCount(props: {openCart: () => void}) {
const rootData = useRootLoaderData();

return (
<Suspense fallback={<Badge count={0} />}>
<Suspense fallback={<Badge count={0} openCart={props.openCart} />}>
<Await resolve={rootData?.cart}>
{(cart) => <Badge count={cart?.totalQuantity || 0} />}
{(cart) => (
<Badge count={cart?.totalQuantity || 0} openCart={props.openCart} />
)}
</Await>
</Suspense>
);
}

function Badge(props: {count: number}) {
function Badge(props: {count: number; openCart: () => void}) {
const {count} = props;
const isHydrated = useIsHydrated();
const path = useLocalePath({path: '/cart'});
Expand All @@ -46,12 +48,16 @@ function Badge(props: {count: number}) {
[count, isHydrated],
);

return (
<Link
className="focus:ring-primary/5 relative flex size-8 items-center justify-center"
prefetch="intent"
to={path}
>
const buttonClass = cx([
'relative flex size-8 items-center justify-center focus:ring-primary/5',
]);

return isHydrated ? (
<button className={buttonClass} onClick={props.openCart}>
{BadgeCounter}
</button>
) : (
<Link className={buttonClass} prefetch="intent" to={path}>
{BadgeCounter}
</Link>
);
Expand Down
50 changes: 45 additions & 5 deletions templates/hydrogen-theme/app/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,58 @@
import type {CSSProperties} from 'react';

import {Link} from '@remix-run/react';
import {Await, Link} from '@remix-run/react';
import {CartForm} from '@shopify/hydrogen';
import {cx} from 'class-variance-authority';
import {type CSSProperties, Suspense, useEffect} from 'react';

import {useCartFetchers} from '~/hooks/useCartFetchers';
import {useRootLoaderData} from '~/hooks/useRootLoaderData';
import {useSanityRoot} from '~/hooks/useSanityRoot';
import {useSettingsCssVars} from '~/hooks/useSettingsCssVars';

import {Drawer, useDrawer} from '../Drawer';
import {Cart} from '../cart/Cart';
import {headerVariants} from '../cva/header';
import {Navigation} from '../navigation/Navigation';
import {CartCount} from './CartCount';
import {Logo} from './Logo';

export function Header() {
const {
closeDrawer: closeCart,
isOpen: isCartOpen,
openDrawer: openCart,
} = useDrawer();

const addToCartFetchers = useCartFetchers(CartForm.ACTIONS.LinesAdd);

// toggle cart drawer when adding to cart
useEffect(() => {
if (isCartOpen || !addToCartFetchers.length) return;
openCart();
}, [addToCartFetchers, isCartOpen, openCart]);

return (
<>
<CartDrawer isOpen={isCartOpen} onClose={closeCart} />
<DesktopHeader openCart={openCart} />
</>
);
}

function CartDrawer({isOpen, onClose}: {isOpen: boolean; onClose: () => void}) {
const rootData = useRootLoaderData();

return (
<Drawer heading="Cart" onClose={onClose} open={isOpen} openFrom="right">
<Suspense fallback={<div>Loading...</div>}>
<Await resolve={rootData?.cart}>
{(cart) => <Cart cart={cart} layout="drawer" onClose={onClose} />}
</Await>
</Suspense>
</Drawer>
);
}

function DesktopHeader(props: {openCart: () => void}) {
const {data} = useSanityRoot();
const header = data?.header;
const logoWidth = header?.desktopLogoWidth
Expand All @@ -21,7 +62,6 @@ export function Header() {
const cssVars = useSettingsCssVars({
settings: header,
});

return (
<header
className={cx([
Expand All @@ -47,7 +87,7 @@ export function Header() {
</Link>
<div className="flex items-center gap-3">
<Navigation data={header?.menu} />
<CartCount />
<CartCount openCart={props.openCart} />
</div>
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions templates/hydrogen-theme/app/hooks/useCartFetchers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {useFetchers} from '@remix-run/react';
import {CartForm} from '@shopify/hydrogen';

export function useCartFetchers(actionName: string) {
const fetchers = useFetchers();
const cartFetchers = [];

for (const fetcher of fetchers) {
if (fetcher.formData) {
const formInputs = CartForm.getFormInput(fetcher.formData);
if (formInputs.action === actionName) {
cartFetchers.push(fetcher);
}
}
}
return cartFetchers;
}

0 comments on commit 64e98c2

Please sign in to comment.