Skip to content

Commit

Permalink
feat(core): add delete address functionality for account
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-alexsaiannyi committed May 16, 2024
1 parent 92a5189 commit b20168c
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-rice-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

Add delete address functionality for account
10 changes: 8 additions & 2 deletions core/app/[locale]/(default)/(faceted)/_components/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ export const Pagination = ({
const beforeSearchParams = new URLSearchParams(searchParams);

beforeSearchParams.delete('after');
beforeSearchParams.set('before', String(startCursor));

if (startCursor) {
beforeSearchParams.set('before', String(startCursor));
}

const afterSearchParams = new URLSearchParams(searchParams);

afterSearchParams.delete('before');
afterSearchParams.set('after', String(endCursor));

if (endCursor) {
afterSearchParams.set('after', String(endCursor));
}

return (
<nav aria-label="Pagination" className="my-6 text-center text-primary">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use server';

import { revalidatePath } from 'next/cache';

import { deleteCustomerAddress } from '~/client/mutations/delete-customer-address';

import { State } from './submit-customer-change-password-form';

export const deleteAddress = async (addressId: number): Promise<State> => {
try {
const response = await deleteCustomerAddress(addressId);

revalidatePath('/account/addresses', 'page');

if (response.errors.length === 0) {
return { status: 'success', message: 'The address has been deleted' };
}

return {
status: 'error',
message: response.errors.map((error) => error.message).join('\n'),
};
} 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,38 @@
'use client';

import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react';

import { State as AccountState } from '../_actions/submit-customer-change-password-form';

export const AccountStatusContext = createContext<{
accountState: AccountState;
setAccountState: (state: AccountState | ((prevState: AccountState) => AccountState)) => void;
} | null>(null);

export const AccountStatusProvider = ({ children }: PropsWithChildren) => {
const [accountState, setAccountState] = useState<AccountState>({ status: 'idle', message: '' });

useEffect(() => {
if (accountState.status !== 'idle') {
setTimeout(() => {
setAccountState({ status: 'idle', message: '' });
}, 3000);
}
}, [accountState, setAccountState]);

return (
<AccountStatusContext.Provider value={{ accountState, setAccountState }}>
{children}
</AccountStatusContext.Provider>
);
};

export function useAccountStatusContext() {
const context = useContext(AccountStatusContext);

if (!context) {
throw new Error('useAccountStatusContext must be used within a AccountStatusProvider');
}

return context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ type CustomerAddresses = NonNullable<Awaited<ReturnType<typeof getCustomerAddres

interface Props {
addresses: CustomerAddresses['addresses'];
addressesCount: number;
pageInfo: CustomerAddresses['pageInfo'];
title: TabType;
}

export const AddressesContent = async ({ addresses, pageInfo, title }: Props) => {
export const AddressesContent = async ({ addresses, addressesCount, pageInfo, title }: Props) => {
const locale = await getLocale();
const tPagination = await getTranslations({ locale, namespace: 'Pagination' });
const { hasNextPage, hasPreviousPage, startCursor, endCursor } = pageInfo;

return (
<>
<TabHeading heading={title} locale={locale} />
<AddressesList customerAddressBook={addresses} />
<AddressesList
addressesCount={addressesCount}
customerAddressBook={addresses}
key={endCursor}
/>
<Pagination
endCursor={endCursor}
hasNextPage={hasNextPage}
Expand Down
155 changes: 110 additions & 45 deletions core/app/[locale]/(default)/account/[tab]/_components/addresses-list.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,136 @@
'use client';

import { useTranslations } from 'next-intl';
import { useState } from 'react';

import { getCustomerAddresses } from '~/client/queries/get-customer-addresses';
import { Link } from '~/components/link';
import { Button } from '~/components/ui/button';
import { Message } from '~/components/ui/message';

type Addresses = NonNullable<Awaited<ReturnType<typeof getCustomerAddresses>>>['addresses'];
import { deleteAddress } from '../_actions/delete-address';
import { State } from '../_actions/submit-customer-change-password-form';

interface Props {
customerAddressBook: Addresses;
import { useAccountStatusContext } from './account-status-provider';
import { Modal } from './modal';

export type Addresses = NonNullable<Awaited<ReturnType<typeof getCustomerAddresses>>>['addresses'];

interface AddressChangeProps {
addressId: number;
isAddressRemovable: boolean;
onDelete: (state: Addresses | ((prevState: Addresses) => Addresses)) => void;
onAddressChange: (state: State | ((prevState: State) => State)) => void;
}

const AddressChangeButtons = () => {
const AddressChangeButtons = ({
addressId,
isAddressRemovable,
onDelete,
onAddressChange,
}: AddressChangeProps) => {
const t = useTranslations('Account.Addresses');

const handleDeleteAddress = async () => {
const { status, message } = await deleteAddress(addressId);

if (status === 'success') {
onDelete((prevAddressBook) =>
prevAddressBook.filter(({ entityId }) => entityId !== addressId),
);

onAddressChange({ status, message: 'Address deleted from your account.' });
}

if (status === 'error') {
onAddressChange({ status, message });
}

window.scrollTo({
top: 0,
behavior: 'smooth',
});
};

return (
<div className="my-2 flex w-fit gap-x-2 divide-y-0">
<Button aria-label={t('editButton')} variant="secondary">
{t('editButton')}
</Button>
<Button aria-label={t('deleteButton')} variant="subtle">
{t('deleteButton')}
</Button>
<Modal
actionHandler={handleDeleteAddress}
confirmationText={t('confirmDeleteAddress')}
title={t('deleteModalTitle')}
>
<Button aria-label={t('deleteButton')} disabled={!isAddressRemovable} variant="subtle">
{t('deleteButton')}
</Button>
</Modal>
</div>
);
};

export const AddressesList = ({ customerAddressBook }: Props) => {
interface Props {
customerAddressBook: Addresses;
addressesCount: number;
}

export const AddressesList = ({ customerAddressBook, addressesCount }: Props) => {
const t = useTranslations('Account.Addresses');
const [addressBook, setAddressBook] = useState(customerAddressBook);
const { accountState, setAccountState } = useAccountStatusContext();

return (
<ul className="mb-12">
{customerAddressBook.map(
({
entityId,
firstName,
lastName,
address1,
address2,
city,
stateOrProvince,
postalCode,
countryCode,
}) => (
<li
className="flex w-full border-collapse flex-col justify-start gap-2 border-t border-gray-200 pb-3 pt-5"
key={entityId}
>
<div className="inline-flex flex-col justify-start text-base">
<p>
{firstName} {lastName}
</p>
<p>{address1}</p>
{Boolean(address2) && <p>{address2}</p>}
<p>
{city}, {stateOrProvince} {postalCode}
</p>
<p>{countryCode}</p>
</div>
<AddressChangeButtons />
</li>
),
<>
{(accountState.status === 'error' || accountState.status === 'success') && (
<Message className="mb-8 w-full text-gray-500" variant={accountState.status}>
<p>{accountState.message}</p>
</Message>
)}
<li className="flex w-full border-collapse flex-col justify-start gap-2 border-t border-gray-200 pt-8">
<Button aria-label={t('addNewAddress')} asChild className="w-fit hover:text-white">
<Link href="/account/add-new-address">{t('addNewAddress')}</Link>
</Button>
</li>
</ul>

<ul className="mb-12">
{addressBook.map(
({
entityId,
firstName,
lastName,
address1,
address2,
city,
stateOrProvince,
postalCode,
countryCode,
}) => (
<li
className="flex w-full border-collapse flex-col justify-start gap-2 border-t border-gray-200 pb-3 pt-5"
key={entityId}
>
<div className="inline-flex flex-col justify-start text-base">
<p>
{firstName} {lastName}
</p>
<p>{address1}</p>
{Boolean(address2) && <p>{address2}</p>}
<p>
{city}, {stateOrProvince} {postalCode}
</p>
<p>{countryCode}</p>
</div>
<AddressChangeButtons
addressId={entityId}
isAddressRemovable={addressesCount > 1}
onAddressChange={setAccountState}
onDelete={setAddressBook}
/>
</li>
),
)}
<li className="flex w-full border-collapse flex-col justify-start gap-2 border-t border-gray-200 pt-8">
<Button aria-label={t('addNewAddress')} asChild className="w-fit hover:text-white">
<Link href="/account/add-address">{t('addNewAddress')}</Link>
</Button>
</li>
</ul>
</>
);
};
70 changes: 70 additions & 0 deletions core/app/[locale]/(default)/account/[tab]/_components/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import { X } from 'lucide-react';
import { MouseEventHandler, PropsWithChildren } from 'react';

import { Button } from '~/components/ui/button';
import {
Dialog,
DialogAction,
DialogCancel,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from '~/components/ui/dialog';

interface Props extends PropsWithChildren {
actionHandler?: MouseEventHandler<HTMLButtonElement>;
title: string;
descriptionText?: string;
confirmationText?: string;
abortText?: string;
}

export const Modal = ({
abortText = 'Cancel',
actionHandler,
confirmationText = 'OK',
descriptionText,
title,
children,
}: Props) => {
return (
<Dialog>
<DialogTrigger aria-controls="modal-content" asChild>
{children}
</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent className="w-full sm:w-8/12 md:w-6/12 lg:w-5/12" id="modal-content">
<div className="flex justify-between gap-4 p-6">
<DialogTitle>{title}</DialogTitle>
<DialogCancel asChild>
<Button className="ms-auto w-min p-2" type="button" variant="subtle">
<X>
<title>{abortText}</title>
</X>
</Button>
</DialogCancel>
</div>
{Boolean(descriptionText) && <DialogDescription>{descriptionText}</DialogDescription>}
<div className="flex flex-col gap-2 p-6 lg:flex-row">
<DialogAction asChild>
<Button className="w-full lg:w-fit" onClick={actionHandler} variant="primary">
{confirmationText}
</Button>
</DialogAction>
<DialogCancel asChild>
<Button className="w-full lg:w-fit" variant="subtle">
{abortText}
</Button>
</DialogCancel>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
);
};
Loading

0 comments on commit b20168c

Please sign in to comment.