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

Add cart drawer #67

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;
}