Skip to content

Commit

Permalink
update nav-link component & add translations for shipping estimator
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-alexsaiannyi committed Feb 29, 2024
1 parent 40b32d3 commit 500cf9e
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useTranslations } from 'next-intl';
import { createContext, Dispatch, SetStateAction, useState } from 'react';

import { getCart } from '~/client/queries/get-cart';
Expand Down Expand Up @@ -63,6 +64,7 @@ export const CheckoutSummary = ({
shippingCountries: ShippingCountries;
shippingCosts: ShippingCosts | null;
}) => {
const t = useTranslations('Cart.CheckoutSummary');
const [isShippingMethodSelected, setIsShippingMethodSelected] = useState(false);
const [checkoutSummary, updateCheckoutSummary] = useState<CheckoutSummary>({
...cart,
Expand Down Expand Up @@ -104,7 +106,7 @@ export const CheckoutSummary = ({
}}
>
<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="text-base font-semibold">Subtotal</span>
<span className="text-base font-semibold">{t('subTotal')}</span>
<span className="text-base">
{currencyFormatter.format(cart.totalExtendedListPrice.value)}
</span>
Expand All @@ -123,14 +125,14 @@ export const CheckoutSummary = ({
/>

<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="text-base font-semibold">Discounts</span>
<span className="text-base font-semibold">{t('discounts')}</span>
<span className="text-base">
{currencyFormatter.format(checkoutSummary.totalDiscountedAmount.value)}
</span>
</div>

<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="text-h5">Grand total</span>
<span className="text-h5">{t('grandTotal')}</span>
<span className="text-h5">
{currencyFormatter.format(
checkoutSummary.totalExtendedSalePrice.value +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { Button } from '@bigcommerce/components/button';
import { useTranslations } from 'next-intl';
import { useContext, useState } from 'react';

import { ShippingInfo } from '~/components/forms';
Expand All @@ -16,6 +17,7 @@ export const ShippingEstimator = ({
const [isOpened, setIsOpened] = useState(false);
const [wasShippingMethodSelectedBefore, setWasShippingMethodSelectedBefore] =
useState(isShippingMethodSelected);
const t = useTranslations('Cart.CheckoutSummary');

const currencyFormatter = createCurrencyFormatter(currencyCode);

Expand All @@ -32,7 +34,7 @@ export const ShippingEstimator = ({
<div className="flex flex-col justify-between gap-4 border-t border-t-gray-200 py-4">
<div className="w-full">
<p className="flex justify-between">
<span className="text-base font-semibold">Shipping Cost</span>
<span className="text-base font-semibold">{t('shippingCost')}</span>
<span className="text-base">
{currencyFormatter.format(shippingCosts.shippingCostTotal)}
</span>
Expand All @@ -44,7 +46,7 @@ export const ShippingEstimator = ({
onClick={() => setIsOpened((open) => !open)}
variant="subtle"
>
Change
{t('change')}
</Button>
</p>
<ShippingInfo
Expand All @@ -54,7 +56,7 @@ export const ShippingEstimator = ({
/>
</div>
<div className="inline-flex justify-between border-t border-t-gray-200 pt-4">
<span className="text-base font-semibold">Handling Cost</span>
<span className="text-base font-semibold">{t('handlingCost')}</span>
<span className="text-base">
{currencyFormatter.format(shippingCosts.handlingCostTotal)}
</span>
Expand All @@ -64,13 +66,13 @@ export const ShippingEstimator = ({
<div className="flex flex-col justify-between gap-4 border-t border-t-gray-200 py-4">
<div className="inline-flex w-full flex-col justify-between">
<div className="inline-flex items-center justify-between">
<span className="text-base font-semibold">Shipping</span>
<span className="text-base font-semibold">{t('shippingCost')}</span>
<Button
className="w-fit p-0 text-base text-blue-primary"
onClick={() => setIsOpened((open) => !open)}
variant="subtle"
>
{isOpened ? 'Cancel' : 'Add'}
{isOpened ? t('cancel') : t('add')}
</Button>
</div>
<ShippingInfo cartItems={shippingItems} isVisible={isOpened} />
Expand Down
20 changes: 12 additions & 8 deletions apps/core/app/[locale]/(default)/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Button } from '@bigcommerce/components/button';
import pick from 'lodash.pick';
import { Trash2 as Trash } from 'lucide-react';
import { cookies } from 'next/headers';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { NextIntlClientProvider, useTranslations } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { Suspense } from 'react';

import { getCheckoutUrl } from '~/client/management/get-checkout-url';
Expand Down Expand Up @@ -51,6 +52,7 @@ interface Props {

export default async function CartPage({ params: { locale } }: Props) {
const t = await getTranslations({ locale, namespace: 'Cart' });
const dictionary = await getMessages({ locale });
const cartId = cookies().get('cartId')?.value;
const shippingCostData = cookies().get('shippingCosts')?.value;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Expand Down Expand Up @@ -180,12 +182,14 @@ export default async function CartPage({ params: { locale } }: Props) {
</ul>

<div className="col-span-1 col-start-2 lg:col-start-3">
<CheckoutSummary
cart={cart}
key={cart.totalExtendedListPrice.value}
shippingCosts={shippingCosts}
shippingCountries={shippingCountries}
/>
<NextIntlClientProvider locale={locale} messages={pick(dictionary, 'Cart')}>
<CheckoutSummary
cart={cart}
key={cart.totalExtendedListPrice.value}
shippingCosts={shippingCosts}
shippingCountries={shippingCountries}
/>
</NextIntlClientProvider>

<Suspense fallback={t('loading')}>
<CheckoutButton cartId={cartId} label={t('proceedToCheckout')} />
Expand Down
1 change: 0 additions & 1 deletion apps/core/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export default async function NotFound() {
messages={pick(dictionary, 'Product')}
>
<ProductCard
key={product.entityId}
product={product}
showCart={false}
showCompare={false}
Expand Down
2 changes: 1 addition & 1 deletion apps/core/components/compare-drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const CompareDrawer = () => {
return (
<div className="fixed bottom-0 start-0 w-full border-t border-gray-200 bg-white p-6 md:pe-0">
<div className="hidden md:flex">
<CompareLink products={products} />
<CompareLink key={products.toString()} products={products} />
<ul className="flex overflow-auto">
{products.map((product) => {
return (
Expand Down
2 changes: 1 addition & 1 deletion apps/core/components/forms/shipping-cost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useFormStatus } from 'react-dom';
import {
CheckoutContext,
createCurrencyFormatter,
} from '~/app/(default)/cart/_components/checkout-summary';
} from '~/app/[locale]/(default)/cart/_components/checkout-summary';
import { addCheckoutShippingInfo } from '~/client/mutations/add-checkout-shipping-info';
import { ExistingResultType } from '~/client/util';
import { cn } from '~/lib/utils';
Expand Down
2 changes: 1 addition & 1 deletion apps/core/components/forms/shipping-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Loader2 as Spinner } from 'lucide-react';
import { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react';
import { useFormStatus } from 'react-dom';

import { CheckoutContext } from '~/app/(default)/cart/_components/checkout-summary';
import { CheckoutContext } from '~/app/[locale]/(default)/cart/_components/checkout-summary';
import { ExistingResultType } from '~/client/util';
import { cn } from '~/lib/utils';

Expand Down
68 changes: 56 additions & 12 deletions apps/core/components/nav-link/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
'use client';

import { ComponentProps, ElementRef, forwardRef } from 'react';
import { ComponentPropsWithRef, ElementRef, forwardRef, useReducer } from 'react';

import { cn } from '~/lib/utils';

import { Link } from '../../navigation';
import { Link, useRouter } from '../../navigation';

type NavLinkProp = ComponentProps<typeof Link>;
type NavLinkType = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NavLinkProp> &
NavLinkProp & {
children?: React.ReactNode;
} & React.RefAttributes<HTMLAnchorElement>;
type NavtLinkProps = Omit<ComponentPropsWithRef<typeof Link>, 'prefetch'>;

interface PrefetchOptions {
prefetch?: 'hover' | 'viewport' | 'none';
prefetchKind?: 'auto' | 'full';
}

type Props = NavtLinkProps & PrefetchOptions;

/**
* This custom `Link` is based on Next-Intl's `Link` component
* https://next-intl-docs.vercel.app/docs/routing/navigation#link
* which adds automatically prefixes for the href with the current locale as necessary
* and etends with additional prefetching controls, making navigation
* prefetching more adaptable to different use cases. By offering `prefetch` and `prefetchKind`
* props, it grants explicit management over when and how prefetching occurs, defaulting to 'hover' for
* prefetch behavior and 'auto' for prefetch kind. This approach provides a balance between optimizing
* page load performance and resource usage. https://nextjs.org/docs/app/api-reference/components/link#prefetch
*/
export const NavLink = forwardRef<ElementRef<'a'>, Props>(
({ href, prefetch = 'hover', prefetchKind = 'auto', children, className, ...rest }, ref) => {
const router = useRouter();
const [prefetched, setPrefetched] = useReducer(() => true, false);
const computedPrefetch = computePrefetchProp({ prefetch, prefetchKind });

const triggerPrefetch = () => {
if (prefetched) {
return;
}

// PrefetchKind enum is not exported
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
router.prefetch(href.toString(), { kind: prefetchKind });
setPrefetched();
};

// This component wraps next/link and automatically prefixes the href with the current locale as necessary.
// https://next-intl-docs.vercel.app/docs/routing/navigation#link
export const NavLink = forwardRef<ElementRef<'a'>, NavLinkType>(
({ href, prefetch = false, children, className, ...rest }, ref) => {
return (
<Link
className={cn(
' hover:text-blue-primary focus:outline-none focus:ring-4 focus:ring-blue-primary/20',
className,
)}
href={href}
prefetch={prefetch}
onMouseEnter={prefetch === 'hover' ? triggerPrefetch : undefined}
onTouchStart={prefetch === 'hover' ? triggerPrefetch : undefined}
prefetch={computedPrefetch}
ref={ref}
{...rest}
>
Expand All @@ -32,3 +61,18 @@ export const NavLink = forwardRef<ElementRef<'a'>, NavLinkType>(
);
},
);

function computePrefetchProp({
prefetch,
prefetchKind,
}: Required<PrefetchOptions>): boolean | undefined {
if (prefetch !== 'viewport') {
return false;
}

if (prefetchKind === 'auto') {
return undefined;
}

return true;
}
16 changes: 12 additions & 4 deletions apps/core/dictionaries/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,20 @@
"heading": "(DE) Your cart",
"empty": "(DE) Your cart is empty",
"emptyDetails": "(DE) Looks like you have not addded anything to your cart. Go ahead & explore top categories",
"subTotal": "(DE) Subtotal",
"discounts": "(DE) Discounts",
"grandTotal": "(DE) Grand total",
"removeFromCart": "(DE) Remove product from cart",
"proceedToCheckout": "(DE) Proceed to checkout",
"loading": "(DE) Loading..."
"loading": "(DE) Loading...",
"CheckoutSummary": {
"subTotal": "(DE) Subtotal",
"discounts": "(DE) Discounts",
"grandTotal": "(DE) Grand total",
"shipping": "(DE) Shipping",
"shippingCost": "(DE) Shipping Cost",
"handlingCost": "(DE) Handling Cost",
"add": "(DE) Add",
"change": "(DE) Change",
"cancel": "(DE) Cancel"
}
},
"Blog": {
"heading": "(DE) i18n",
Expand Down
16 changes: 12 additions & 4 deletions apps/core/dictionaries/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,20 @@
"heading": "(EN) Your cart",
"empty": "(EN) Your cart is empty",
"emptyDetails": "(EN) Looks like you have not addded anything to your cart. Go ahead & explore top categories",
"subTotal": "(EN) Subtotal",
"discounts": "(EN) Discounts",
"grandTotal": "(EN) Grand total",
"removeFromCart": "(EN) Remove product from cart",
"proceedToCheckout": "(EN) Proceed to checkout",
"loading": "(EN) Loading..."
"loading": "(EN) Loading...",
"CheckoutSummary": {
"subTotal": "(EN) Subtotal",
"discounts": "(EN) Discounts",
"grandTotal": "(EN) Grand total",
"shipping": "(EN) Shipping",
"shippingCost": "(EN) Shipping Cost",
"handlingCost": "(EN) Handling Cost",
"add": "(EN) Add",
"change": "(EN) Change",
"cancel": "(EN) Cancel"
}
},
"Blog": {
"heading": "(EN) i18n",
Expand Down
16 changes: 6 additions & 10 deletions apps/core/middlewares/with-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,18 @@ interface StorefrontStatusCache {
expiryTime: number;
}

const clearPathFromLocale = (path: string) => {
// TODO: Handle case when cookie is empty during first load
const selectedLocale = cookies().get('NEXT_LOCALE')?.value;
let locale: string;
const clearLocaleFromPath = (path: string) => {
let res: string;

if (localePrefix === LocalePrefixes.ALWAYS) {
res = selectedLocale ? `/${path.split('/').slice(2).join('/')}` : path;
res = locale ? `/${path.split('/').slice(2).join('/')}` : path;

return res;
}

if (localePrefix === LocalePrefixes.ASNEEDED) {
res =
selectedLocale && selectedLocale !== defaultLocale
? `/${path.split('/').slice(2).join('/')}`
: path;
res = locale && locale !== defaultLocale ? `/${path.split('/').slice(2).join('/')}` : path;

return res;
}
Expand Down Expand Up @@ -118,7 +114,7 @@ const updateStatusCache = async (event: NextFetchEvent): Promise<StorefrontStatu

const getRouteInfo = async (request: NextRequest, event: NextFetchEvent) => {
try {
const pathname = clearPathFromLocale(request.nextUrl.pathname);
const pathname = clearLocaleFromPath(request.nextUrl.pathname);

let [routeCache, statusCache] = await kv.mget<RouteCache | StorefrontStatusCache>(
routeCacheKvKey(pathname),
Expand Down Expand Up @@ -248,7 +244,7 @@ export const withRoutes: MiddlewareFactory = () => {

default: {
const { pathname } = new URL(request.url);
const cleanPathName = clearPathFromLocale(pathname);
const cleanPathName = clearLocaleFromPath(pathname);

if (cleanPathName === '/' && postfix) {
url = `/${locale}${postfix}`;
Expand Down
1 change: 0 additions & 1 deletion apps/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"@testing-library/react": "^14.1.2",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.pick": "^4.4.9",
"@types/negotiator": "^0.6.3",
"@types/node": "^18.17.12",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
Expand Down
7 changes: 0 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 500cf9e

Please sign in to comment.