Skip to content

Commit

Permalink
feat(core): add orders-all page for account (#1167)
Browse files Browse the repository at this point in the history
feat(core): add order details components & update page data

feat(core): add translations for order details page, small fixes for multi consignments order

refactor(core): update header and account home page links after refactoring

fix(core): simplify UI with respect to available data

fix(core): move orders connection under customer in respect with api

fix(core): update quantity of items in order truncated card

refactor(core): update getTranslations for orders

fix(core): separate promotions and coupons for order summary

refactor(core): update OrderItemFragment according to API changes

refactor(core): update in respect with storefront token changes

feat(core): add images for order items

refactor(core): Next15 upgrade asynchronous searchparams for orders page

feat(core): add path to PDP for Orders
  • Loading branch information
bc-alexsaiannyi committed Nov 13, 2024
1 parent 1971781 commit f6f7da9
Show file tree
Hide file tree
Showing 12 changed files with 1,231 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-ties-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

Add orders for cusomter account. Now customer can open orders history or move to specific order details.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Link } from '~/components/link';
import { usePathname } from '~/i18n/routing';
import { cn } from '~/lib/utils';

const tabList = ['addresses', 'settings'] as const;
const tabList = ['orders', 'addresses', 'settings'] as const;

export type TabType = (typeof tabList)[number];

Expand All @@ -18,6 +18,7 @@ export const TabNavigation = () => {
const tabsTitles = {
addresses: t('addresses'),
settings: t('settings'),
orders: t('orders'),
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { getFormatter, getTranslations } from 'next-intl/server';

import { Link } from '~/components/link';
import { Button } from '~/components/ui/button';
import { cn } from '~/lib/utils';

import { OrderDetailsDataType } from '../page-data';

import { assembleProductData, ProductSnippet } from './product-snippet';

const OrderState = async ({ orderState }: { orderState: OrderDetailsDataType['orderState'] }) => {
const t = await getTranslations('Account.Orders');
const format = await getFormatter();
const { orderId, orderDate, status } = orderState;

return (
<div className="mb-6 flex flex-col gap-6 md:flex-row">
<div>
<h2 className="mb-2 text-3xl font-bold lg:text-4xl">
{t('orderNumber')}
{orderId}
</h2>
<p>
{format.dateTime(new Date(orderDate.utc), {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
<p className="align-center flex h-fit justify-center gap-2.5 rounded-3xl bg-secondary/10 px-4 py-1.5 font-semibold text-primary">
{status.label}
</p>
</div>
);
};

const OrderSummaryInfo = async ({
summaryInfo,
}: {
summaryInfo: OrderDetailsDataType['summaryInfo'];
}) => {
const t = await getTranslations('Account.Orders');
const format = await getFormatter();
const { subtotal, shipping, tax, discounts, grandTotal } = summaryInfo;
const { nonCouponDiscountTotal, couponDiscounts } = discounts;

return (
<div className="border border-gray-200 p-6">
<p className="pb-4 text-lg font-semibold">{t('orderSummary')}</p>
<div className="flex border-collapse flex-col gap-2 border-y border-gray-200 py-4">
<p className="flex justify-between">
<span>{t('orderSubtotal')}</span>
<span>
{format.number(subtotal.value, {
style: 'currency',
currency: subtotal.currencyCode,
})}
</span>
</p>
{nonCouponDiscountTotal.value > 0 && (
<p className="flex justify-between">
<span>{t('orderDiscount')}</span>
<span>
-
{format.number(nonCouponDiscountTotal.value, {
style: 'currency',
currency: nonCouponDiscountTotal.currencyCode,
})}
</span>
</p>
)}
{couponDiscounts.map(({ couponCode, discountedAmount }, index) => (
<p className="flex justify-between" key={index}>
<span>{t('orderAppliedCoupon', { code: couponCode })}</span>
<span>
-
{format.number(discountedAmount.value, {
style: 'currency',
currency: discountedAmount.currencyCode,
})}
</span>
</p>
))}
<p className="flex justify-between">
<span>{t('orderShipping')}</span>
<span>
{format.number(shipping.value, {
style: 'currency',
currency: shipping.currencyCode,
})}
</span>
</p>
<p className="flex justify-between">
<span>{t('orderTax')}</span>
<span>
{format.number(tax.value, {
style: 'currency',
currency: tax.currencyCode,
})}
</span>
</p>
</div>
<div className="pt-4 text-base font-semibold">
<p className="flex justify-between">
<span>{t('orderGrandtotal')}</span>
<span>
{format.number(grandTotal.value, {
style: 'currency',
currency: grandTotal.currencyCode,
})}
</span>
</p>
</div>
{/* TODO: add manage-order buttons */}
</div>
);
};
const combineAddressInfo = (
address: NonNullable<OrderDetailsDataType['consignments']['shipping']>[number]['shippingAddress'],
) => {
const { firstName, lastName, address1, city, stateOrProvince, postalCode, country } = address;
const fullName = `${firstName ?? ''} ${lastName ?? ''}`;
const addressLine = address1 ?? '';
const cityWithZipCode = `${city ?? ''}, ${stateOrProvince} ${postalCode}`;
const shippingCountry = country;

return [fullName, addressLine, cityWithZipCode, shippingCountry];
};
const combineShippingMethodInfo = async (
shipment?: NonNullable<
NonNullable<OrderDetailsDataType['consignments']['shipping']>[number]['shipments']
>[number],
) => {
if (!shipment) {
return [];
}

const t = await getTranslations('Account.Orders');
const format = await getFormatter();
const { shippingProviderName, shippingMethodName, shippedAt } = shipment;
const providerWithMethod = `${shippingProviderName} - ${shippingMethodName}`;
const shippedDate = `${t('shippedDate')} ${format.dateTime(new Date(shippedAt.utc), {
year: 'numeric',
month: 'long',
day: 'numeric',
})}`;

return [providerWithMethod, shippedDate];
};

const ShippingInfo = async ({
consignments,
isMultiConsignments,
shippingNumber,
}: {
consignments: OrderDetailsDataType['consignments'];
isMultiConsignments: boolean;
shippingNumber?: number;
}) => {
const t = await getTranslations('Account.Orders');
const shippingConsignments = consignments.shipping;

if (!shippingConsignments) {
return;
}

let customerShippingAddress: string[] = [];
let customerShippingMethod: string[] = [];
let trackingData;

if (!isMultiConsignments && shippingConsignments[0]?.shippingAddress) {
trackingData = shippingConsignments[0].shipments[0]?.tracking;
customerShippingAddress = combineAddressInfo(shippingConsignments[0].shippingAddress);
customerShippingMethod = await combineShippingMethodInfo(shippingConsignments[0].shipments[0]);
}

if (isMultiConsignments && shippingNumber !== undefined && shippingConsignments[shippingNumber]) {
trackingData = shippingConsignments[shippingNumber].shipments[0]?.tracking;
customerShippingAddress = combineAddressInfo(
shippingConsignments[shippingNumber].shippingAddress,
);
customerShippingMethod = await combineShippingMethodInfo(
shippingConsignments[shippingNumber].shipments[0],
);
}

const trackingNumber =
trackingData && trackingData.__typename !== 'OrderShipmentUrlOnlyTracking'
? trackingData.number
: null;

return (
<div
className={cn(
'border border-gray-200 p-6',
isMultiConsignments && 'flex flex-col gap-4 border-none md:flex-row md:gap-16',
)}
>
{!isMultiConsignments ? (
<p className="border-b border-gray-200 pb-4 text-lg font-semibold">{t('shippingTitle')}</p>
) : null}
<div className="flex flex-col gap-2 py-4">
<p className="font-semibold">{t('shippingAddress')}</p>
{customerShippingAddress.map((line) => (
<p key={line}>{line}</p>
))}
</div>
<div
className={cn(
'flex flex-col gap-2 border-t border-gray-200 pt-4 text-base',
isMultiConsignments && 'border-0',
)}
>
<p className="font-semibold">{t('shippingMethod')}</p>
{customerShippingMethod.map((line) => (
<p key={line}>{line}</p>
))}
{Boolean(trackingNumber) && (
<Button
aria-label={t('trackOrder')}
asChild
className="justify-start p-0"
variant="subtle"
>
{/* TODO: add link when tracking url available */}
<Link href="#">{trackingNumber}</Link>
</Button>
)}
</div>
</div>
);
};

export const OrderDetails = async ({ data }: { data: OrderDetailsDataType }) => {
const t = await getTranslations('Account.Orders');
const { orderState, summaryInfo, consignments } = data;
const shippingConsignments = consignments.shipping;
const isMultiShippingConsignments = shippingConsignments && shippingConsignments.length > 1;

return (
<div className="mb-14">
<OrderState orderState={orderState} />
<div className="flex flex-col gap-8 lg:flex-row">
<div className="flex flex-col gap-8 lg:w-2/3">
{shippingConsignments?.map((consignment, idx) => {
const { lineItems } = consignment;

return (
<div className="w-full border border-gray-200 p-6" key={idx}>
<p className="border-b border-gray-200 pb-4 text-xl font-semibold lg:text-2xl">
{isMultiShippingConsignments
? `${t('shipmentTitle')} ${idx + 1}/${shippingConsignments.length}`
: t('orderContents')}
</p>
{isMultiShippingConsignments && (
<ShippingInfo
consignments={consignments}
isMultiConsignments={true}
shippingNumber={idx}
/>
)}
<ul className="my-4 flex flex-col gap-4">
{lineItems.map((shipment) => {
return (
<li key={shipment.entityId}>
<ProductSnippet
imagePriority={true}
imageSize="square"
isExtended={true}
product={assembleProductData(shipment)}
/>
</li>
);
})}
</ul>
</div>
);
})}
</div>
<div className="flex grow flex-col gap-8">
<OrderSummaryInfo summaryInfo={summaryInfo} />
{!isMultiShippingConsignments && (
<ShippingInfo consignments={consignments} isMultiConsignments={false} />
)}
{/* TODO: add PaymentInfo component later */}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';

import { Pagination } from '~/components/ui/pagination';

import { TabHeading } from '../../_components/tab-heading';
import { getCustomerOrders, getOrderDetails } from '../page-data';

import { OrderDetails } from './order-details';
import { OrdersList } from './orders-list';

type CustomerOrders = NonNullable<Awaited<ReturnType<typeof getCustomerOrders>>>;

interface Props {
orderId?: string;
orders: CustomerOrders['orders'];
pageInfo: CustomerOrders['pageInfo'];
}

export const OrdersContent = async ({ orderId, orders, pageInfo }: Props) => {
const t = await getTranslations('Account.Orders');
const { hasNextPage, hasPreviousPage, startCursor, endCursor } = pageInfo;

if (orderId) {
const orderData = await getOrderDetails({ orderId });

return orderData ? <OrderDetails data={orderData} /> : notFound();
}

return (
<>
<TabHeading heading="orders" />
{orders.length === 0 ? (
<div className="mx-auto w-fit">{t('noOrders')}</div>
) : (
<OrdersList customerOrders={orders} key={endCursor} />
)}
<div className="mb-14 inline-flex w-full justify-center py-6">
<Pagination
className="my-0 flex inline-flex justify-center text-center"
endCursor={endCursor ?? undefined}
hasNextPage={hasNextPage}
hasPreviousPage={hasPreviousPage}
startCursor={startCursor ?? undefined}
/>
</div>
</>
);
};
Loading

0 comments on commit f6f7da9

Please sign in to comment.