Skip to content

Commit

Permalink
feat(core): add wishlist drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
yurytut1993 committed Sep 6, 2024
1 parent 98d6c79 commit 81115f3
Show file tree
Hide file tree
Showing 12 changed files with 508 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-camels-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

add wishlist drawer
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { graphql } from '~/client/graphql';

export const WishlistSheetFragment = graphql(`
fragment WishlistSheetFragment on WishlistConnection {
edges {
node {
entityId
name
items {
edges {
node {
entityId
product {
name
entityId
}
}
}
}
}
}
}
`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Heart } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { PropsWithChildren, useEffect, useState } from 'react';

import { addWishlistItems } from '~/client/mutations/add-wishlist-items';
import { Sheet } from '~/components/ui/sheet';

import { Button } from '../../../../../../../components/ui/button';
import { AccountStatusProvider } from '../account-status-provider';

import { WishlistSheetContent } from './wishlist-sheet-content';

export type Wishlist = NonNullable<Awaited<ReturnType<typeof addWishlistItems>>>;

interface WishlistSheetProps extends PropsWithChildren {
productId: number;
wishlistsData: Wishlist[];
}

export const WishlistSheet = ({ productId, wishlistsData }: WishlistSheetProps) => {
const t = useTranslations('Account.Wishlist.Sheet');

const [wishlists, setWishlists] = useState(() => {
if (wishlistsData.length === 0) {
return [{ items: [], entityId: 0, name: t('favorites') }];
}

return wishlistsData;
});

const [saved, setSaved] = useState(() => {
if (wishlistsData.length === 0) {
return false;
}

return wishlistsData.some(({ items }) => {
return items.some(({ product }) => product.entityId === productId);
});
});

useEffect(() => {
const firstWishlistWithProduct = wishlists.find(({ items }) =>
items.find(({ product }) => product.entityId === productId),
);

setSaved(!!firstWishlistWithProduct);
}, [productId, setSaved, wishlists]);

return (
<Sheet
side="right"
title={t('title')}
trigger={
<Button type="button" variant="secondary">
<Heart
aria-hidden="true"
className="mx-2"
fill={saved ? 'currentColor' : 'transparent'}
/>
<span>{t(saved ? 'saved' : 'saveToWishlist')}</span>
</Button>
}
>
<AccountStatusProvider>
<WishlistSheetContent
productId={productId}
setWishlists={setWishlists}
wishlists={wishlists}
/>
</AccountStatusProvider>
</Sheet>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use server';

import {
AddWishlistItems,
addWishlistItems as addWishlistItemsMutation,
} from '~/client/mutations/add-wishlist-items';

export const addWishlistItems = async ({ input }: AddWishlistItems) => {
try {
const wishlist = await addWishlistItemsMutation({ input });

if (wishlist) {
return {
status: 'success' as const,
data: wishlist,
};
}
} catch (error: unknown) {
if (error instanceof Error) {
return {
status: 'error' as const,
message: error.message,
};
}
}

return { status: 'error' as const, message: 'Unknown error.' };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use server';

import {
DeleteWishlistItems,
deleteWishlistItems as deleteWishlistItemsMutation,
} from '~/client/mutations/delete-wishlist-items';

export const deleteWishlistItems = async ({ input }: DeleteWishlistItems) => {
try {
const wishlist = await deleteWishlistItemsMutation({ input });

if (wishlist) {
return {
status: 'success',
data: wishlist,
};
}
} catch (error: unknown) {
if (error instanceof Error) {
return {
status: 'error',
message: error.message,
};
}
}

return { status: 'error', message: 'Unknown error.' };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { useFormStatus } from 'react-dom';

import { Button } from '~/components/ui/button';
import { Checkbox } from '~/components/ui/checkbox';
import { Field, Form, FormSubmit } from '~/components/ui/form';
import { Label } from '~/components/ui/label';

import { Wishlist } from '..';

import { addWishlistItems } from './_actions/add-wishlist-items';
import { deleteWishlistItems } from './_actions/delete-wishlist-items';

interface SubmitButtonProps {
disabled: boolean;
}

const SubmitButton = ({ disabled }: SubmitButtonProps) => {
const { pending } = useFormStatus();
const t = useTranslations('Account.Wishlist.Sheet');

return (
<Button
className="relative items-center px-8 py-2"
disabled={disabled}
loading={pending}
loadingText={t('save')}
variant="primary"
>
{t('save')}
</Button>
);
};

interface UpdateWishlistsFormProps {
onWishlistsUpdated: (updatedWishlists: Wishlist[]) => void;
productId: number;
wishlists: Wishlist[];
}

export const UpdateWishlistsForm = ({
onWishlistsUpdated,
productId,
wishlists,
}: UpdateWishlistsFormProps) => {
const [wishlistsList, setWishlistsList] = useState(() => {
return wishlists.map((wishlist) => {
return {
...wishlist,
checked: wishlist.items.some(({ product }) => product.entityId === productId),
upToDate: true,
};
});
});

useEffect(() => {
setWishlistsList((prevWishlistsList) => {
return wishlists.map((wishlist) => {
const prevWishlistsItem = prevWishlistsList.find(
({ entityId }) => entityId === wishlist.entityId,
);

if (prevWishlistsItem) {
return {
...prevWishlistsItem,
};
}

return {
...wishlist,
checked: false,
upToDate: true,
};
});
});
}, [productId, setWishlistsList, wishlists]);

const handleCheckboxChange = (checked: boolean, wishlistId: number) => {
setWishlistsList((prevWishlistsList) => {
const newWishlistItemIdx = prevWishlistsList.findIndex(
(wishlist) => wishlist.entityId === wishlistId,
);

const prevWishlistItem = prevWishlistsList[newWishlistItemIdx];

if (!prevWishlistItem) {
return prevWishlistsList;
}

const newWishlistItem = {
...prevWishlistItem,
checked,
upToDate: !prevWishlistsList[newWishlistItemIdx]?.upToDate,
};

const newWishlistsList = [
...prevWishlistsList.slice(0, newWishlistItemIdx),
newWishlistItem,
...prevWishlistsList.slice(newWishlistItemIdx + 1),
];

return newWishlistsList;
});
};

const onSubmit = async () => {
const wishlistsToUpdate = wishlistsList.filter((wishlist) => {
return !wishlist.upToDate;
});

const result = await Promise.all(
wishlistsToUpdate.map(({ checked, entityId, items }) => {
if (checked) {
return addWishlistItems({
input: {
entityId,
items: [{ productEntityId: productId }],
},
});
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const itemEntityId = items.find(({ product }) => product.entityId === productId)!.entityId;

return deleteWishlistItems({
input: {
entityId,
itemEntityIds: [itemEntityId],
},
});
}),
);

const updatedWishlists = result.reduce<Wishlist[]>((acc, curr) => {
if (curr.data) {
acc.push(curr.data);
}

return acc;
}, []);

setWishlistsList((prevWishlistsList) => {
return prevWishlistsList.map((wishlist) => {
const updatedWishlist = updatedWishlists.find(
({ entityId }) => entityId === wishlist.entityId,
);

return {
...wishlist,
items: updatedWishlist ? [...updatedWishlist.items] : [...wishlist.items],
upToDate: true,
};
});
});

onWishlistsUpdated(updatedWishlists);
};

return (
<Form action={onSubmit} className="mb-3" onSubmit={(e) => e.stopPropagation()}>
<fieldset className="overflow-auto">
{wishlistsList.map(({ entityId, name, checked }) => {
return (
<Field className="mb-6 flex" key={entityId} name={entityId.toString()}>
<Checkbox
checked={checked}
defaultChecked={checked}
id={entityId.toString()}
onCheckedChange={(_checked) => handleCheckboxChange(!!_checked, entityId)}
/>
<Label className="cursor-pointer ps-3" htmlFor={entityId.toString()}>
{name}
</Label>
</Field>
);
})}
</fieldset>
<FormSubmit asChild>
<SubmitButton disabled={!wishlistsList.find((wishlist) => !wishlist.upToDate)} />
</FormSubmit>
</Form>
);
};
Loading

0 comments on commit 81115f3

Please sign in to comment.