diff --git a/examples/core/src/app/(checkout)/checkout/SubmitCheckoutButton.tsx b/examples/core/src/app/(checkout)/checkout/SubmitCheckoutButton.tsx deleted file mode 100644 index 37a12087..00000000 --- a/examples/core/src/app/(checkout)/checkout/SubmitCheckoutButton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { useCheckout } from "./checkout-provider"; -import { StatusButton } from "../../../components/button/StatusButton"; -import { CartResponse } from "@epcc-sdk/sdks-shopper"; - -export function SubmitCheckoutButton({ cart }: { cart: CartResponse }) { - const { handleSubmit, completePayment, isCompleting } = useCheckout(); - - return ( - { - return completePayment(values); - })} - > - {`Pay ${cart.meta?.display_price?.with_tax?.formatted}`} - - ); -} diff --git a/examples/core/src/app/(store)/cart/CartItemWide.tsx b/examples/core/src/app/(store)/cart/CartItemWide.tsx deleted file mode 100644 index b2b35e17..00000000 --- a/examples/core/src/app/(store)/cart/CartItemWide.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ProductThumbnail } from "../account/orders/[orderId]/ProductThumbnail"; -import Link from "next/link"; -import { NumberInput } from "../../../components/number-input/NumberInput"; -import { CartItemProps } from "./CartItem"; -import { RemoveCartItemButton } from "../../../components/cart/RemoveCartItemButton"; - -export function CartItemWide({ item, thumbnail }: CartItemProps) { - return ( -
- {/* Thumbnail */} -
- -
- {/* Details */} -
-
-
- - - {item?.name} - - - - Quantity: {item?.quantity} - - {item?.type === "cart_item" && item?.location && ( - - Location: {item?.location} - - )} -
-
- - -
-
-
-
- - {item?.meta?.display_price?.with_tax?.value?.formatted} - - {item?.meta?.display_price?.without_discount?.value?.amount && - item?.meta?.display_price?.without_discount?.value?.amount !== - item?.meta?.display_price.with_tax?.value?.amount && ( - - {item?.meta?.display_price?.without_discount.value?.formatted} - - )} -
-
- ); -} diff --git a/examples/core/src/app/(store)/cart/CartSidebar.tsx b/examples/core/src/app/(store)/cart/CartSidebar.tsx deleted file mode 100644 index ec435739..00000000 --- a/examples/core/src/app/(store)/cart/CartSidebar.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Separator } from "../../../components/separator/Separator"; -import { CartDiscounts } from "../../../components/cart/CartDiscounts"; -import * as React from "react"; -import { - ItemSidebarPromotions, - ItemSidebarSumTotal, - ItemSidebarTotals, - ItemSidebarTotalsDiscount, - ItemSidebarTotalsSubTotal, - ItemSidebarTotalsTax, -} from "../../../components/checkout-sidebar/ItemSidebar"; -import { CartEntityResponse } from "@epcc-sdk/sdks-shopper"; -import { groupCartItems } from "../../../lib/group-cart-items"; - -export function CartSidebar({ cart }: { cart: CartEntityResponse }) { - const meta = cart.data?.meta!; - const groupedItems = groupCartItems(cart.included?.items ?? []); - - return ( -
- - - - {/* Totals */} - - -
- Shipping - Calculated at checkout -
- - -
- - {/* Sum Total */} - -
- ); -} diff --git a/examples/core/src/app/(store)/cart/page.tsx b/examples/core/src/app/(store)/cart/page.tsx deleted file mode 100644 index 1c320c43..00000000 --- a/examples/core/src/app/(store)/cart/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { YourBag } from "./YourBag"; -import { CartSidebar } from "./CartSidebar"; -import { Button } from "../../../components/button/Button"; -import Link from "next/link"; -import { LockClosedIcon } from "@heroicons/react/24/solid"; -import { getACart } from "@epcc-sdk/sdks-shopper"; -import { CART_COOKIE_NAME } from "../../../lib/cookie-constants"; -import { cookies } from "next/headers"; -import { createElasticPathClient } from "../../../lib/create-elastic-path-client"; -import { TAGS } from "../../../lib/constants"; - -export default async function CartPage() { - const cartCookie = (await cookies()).get(CART_COOKIE_NAME); - const client = createElasticPathClient(); - - if (!cartCookie) { - throw new Error("Cart cookie not found"); - } - - const cartResponse = await getACart({ - path: { - cartID: cartCookie.value, - }, - query: { - include: ["items", "promotions", "tax_items", "custom_discounts"], - }, - client, - next: { - tags: [TAGS.cart], - }, - }); - - if (!cartResponse.data) { - return
Cart items not found
; - } - - const items = cartResponse.data.included?.items!; - - return ( - <> - {items?.length && items.length > 0 ? ( -
- {/* Main Content */} -
-
-

Your Bag

- {/* Cart Items */} - -
-
- {/* Sidebar */} -
- - -
-
- ) : ( - <> - {/* Empty Cart */} -
-

- Empty Cart -

-

Your cart is empty

-
- -
-
- - )} - - ); -} diff --git a/examples/core/src/app/(auth)/account-member-credentials-schema.ts b/examples/core/src/app/[lang]/(auth)/account-member-credentials-schema.ts similarity index 100% rename from examples/core/src/app/(auth)/account-member-credentials-schema.ts rename to examples/core/src/app/[lang]/(auth)/account-member-credentials-schema.ts diff --git a/examples/core/src/app/(auth)/actions.ts b/examples/core/src/app/[lang]/(auth)/actions.ts similarity index 83% rename from examples/core/src/app/(auth)/actions.ts rename to examples/core/src/app/[lang]/(auth)/actions.ts index 8c30377b..7ac98038 100644 --- a/examples/core/src/app/(auth)/actions.ts +++ b/examples/core/src/app/[lang]/(auth)/actions.ts @@ -3,13 +3,13 @@ import { z } from "zod"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../lib/cookie-constants"; -import { retrieveAccountMemberCredentials } from "../../lib/retrieve-account-member-credentials"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; +import { retrieveAccountMemberCredentials } from "src/lib/retrieve-account-member-credentials"; import { revalidatePath, revalidateTag } from "next/cache"; -import { getErrorMessage } from "../../lib/get-error-message"; -import { createElasticPathClient } from "../../lib/create-elastic-path-client"; +import { getErrorMessage } from "src/lib/get-error-message"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { postV2AccountMembersTokens } from "@epcc-sdk/sdks-shopper"; -import { createCookieFromGenerateTokenResponse } from "../../lib/create-cookie-from-generate-token-response"; +import { createCookieFromGenerateTokenResponse } from "src/lib/create-cookie-from-generate-token-response"; const loginSchema = z.object({ email: z.string().email(), @@ -32,7 +32,7 @@ const PASSWORD_PROFILE_ID = process.env.NEXT_PUBLIC_PASSWORD_PROFILE_ID!; const loginErrorMessage = "Failed to login, make sure your email and password are correct"; -export async function login(props: FormData) { +export async function login(props: FormData, lang: string) { const client = createElasticPathClient(); const rawEntries = Object.fromEntries(props.entries()); @@ -77,15 +77,15 @@ export async function login(props: FormData) { }; } - redirect(returnUrl ?? "/"); + redirect(returnUrl ?? (lang ? `/${lang}/` : "/")); } -export async function logout() { +export async function logout(lang?: string) { const cookieStore = await cookies(); cookieStore.delete(ACCOUNT_MEMBER_TOKEN_COOKIE_NAME); - redirect("/"); + redirect(lang ? `/${lang}/` : "/"); } export async function selectedAccount(args: FormData) { @@ -106,8 +106,9 @@ export async function selectedAccount(args: FormData) { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME, ); + const lang = args?.get("lang")?.toString(); if (!accountMemberCredentials) { - redirect("/login"); + redirect(lang ? `/${lang}/login` : "/login"); return; } @@ -141,7 +142,7 @@ export async function selectedAccount(args: FormData) { await Promise.all(promises); } -export async function register(data: FormData) { +export async function register(data: FormData, lang: string) { const client = await createElasticPathClient(); const validatedProps = registerSchema.safeParse( @@ -178,5 +179,5 @@ export async function register(data: FormData) { cookieStore.set(createCookieFromGenerateTokenResponse(result.data)); - redirect("/"); + redirect(lang ? `/${lang}/` : "/"); } diff --git a/examples/core/src/app/(auth)/layout.tsx b/examples/core/src/app/[lang]/(auth)/layout.tsx similarity index 85% rename from examples/core/src/app/(auth)/layout.tsx rename to examples/core/src/app/[lang]/(auth)/layout.tsx index 56503731..15a92cc9 100644 --- a/examples/core/src/app/(auth)/layout.tsx +++ b/examples/core/src/app/[lang]/(auth)/layout.tsx @@ -1,9 +1,9 @@ import localFont from "next/font/local"; import { ReactNode } from "react"; -import { getStoreInitialState } from "../../lib/get-store-initial-state"; +import { getStoreInitialState } from "src/lib/get-store-initial-state"; import { Providers } from "../providers"; import clsx from "clsx"; -import { createElasticPathClient } from "../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME; const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL @@ -23,7 +23,7 @@ export const metadata = { }; const inter = localFont({ - src: "../../../public/fonts/Inter-VariableFont_opsz,wght.ttf", + src: "../../../../public/fonts/Inter-VariableFont_opsz,wght.ttf", display: "swap", variable: "--font-inter", }); diff --git a/examples/core/src/app/(auth)/login/LoginForm.tsx b/examples/core/src/app/[lang]/(auth)/login/LoginForm.tsx similarity index 84% rename from examples/core/src/app/(auth)/login/LoginForm.tsx rename to examples/core/src/app/[lang]/(auth)/login/LoginForm.tsx index a89c4a5c..3d5d434e 100644 --- a/examples/core/src/app/(auth)/login/LoginForm.tsx +++ b/examples/core/src/app/[lang]/(auth)/login/LoginForm.tsx @@ -1,16 +1,18 @@ "use client"; import { login } from "../actions"; -import { Label } from "../../../components/label/Label"; -import { Input } from "../../../components/input/Input"; -import { FormStatusButton } from "../../../components/button/FormStatusButton"; +import { Label } from "src/components/label/Label"; +import { Input } from "src/components/input/Input"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; import { useState } from "react"; +import { useParams } from "next/navigation"; export function LoginForm({ returnUrl }: { returnUrl?: string }) { + const { lang } = useParams(); const [error, setError] = useState(undefined); async function loginAction(formData: FormData) { - const result = await login(formData); + const result = await login(formData, lang as string); if ("error" in result) { setError(result.error); diff --git a/examples/core/src/app/(auth)/login/page.tsx b/examples/core/src/app/[lang]/(auth)/login/page.tsx similarity index 72% rename from examples/core/src/app/(auth)/login/page.tsx rename to examples/core/src/app/[lang]/(auth)/login/page.tsx index 779f82fd..8d306b0b 100644 --- a/examples/core/src/app/(auth)/login/page.tsx +++ b/examples/core/src/app/[lang]/(auth)/login/page.tsx @@ -1,31 +1,35 @@ -import EpLogo from "../../../components/icons/ep-logo"; +import EpLogo from "src/components/icons/ep-logo"; import { cookies } from "next/headers"; -import { isAccountMemberAuthenticated } from "../../../lib/is-account-member-authenticated"; +import { isAccountMemberAuthenticated } from "src/lib/is-account-member-authenticated"; import { redirect } from "next/navigation"; -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; import { LoginForm } from "./LoginForm"; export default async function Login( props: { searchParams: Promise<{ returnUrl?: string }>; + params: Promise<{ lang?: string }>; } ) { const searchParams = await props.searchParams; const { returnUrl } = searchParams; + const params = await props.params; + const { lang } = params; + const cookieStore = await cookies(); if (isAccountMemberAuthenticated(cookieStore)) { - redirect("/account/summary"); + redirect(lang ? `/${lang}/account/summary` : "/account/summary"); } return ( <>
- + - +

Sign in to your account

@@ -36,12 +40,12 @@ export default async function Login(

Not a member?{" "} - Register now! - +

diff --git a/examples/core/src/app/not-found.tsx b/examples/core/src/app/[lang]/(auth)/not-found.tsx similarity index 64% rename from examples/core/src/app/not-found.tsx rename to examples/core/src/app/[lang]/(auth)/not-found.tsx index d97d88f4..b568b0d7 100644 --- a/examples/core/src/app/not-found.tsx +++ b/examples/core/src/app/[lang]/(auth)/not-found.tsx @@ -1,13 +1,13 @@ -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; export default function NotFound() { return (
404 - The page could not be found. - + Back to home - +
); } diff --git a/examples/core/src/app/(auth)/register/page.tsx b/examples/core/src/app/[lang]/(auth)/register/page.tsx similarity index 74% rename from examples/core/src/app/(auth)/register/page.tsx rename to examples/core/src/app/[lang]/(auth)/register/page.tsx index b41553e7..4c3a756c 100644 --- a/examples/core/src/app/(auth)/register/page.tsx +++ b/examples/core/src/app/[lang]/(auth)/register/page.tsx @@ -1,33 +1,40 @@ import { register } from "../actions"; -import EpLogo from "../../../components/icons/ep-logo"; +import EpLogo from "src/components/icons/ep-logo"; import { cookies } from "next/headers"; -import { isAccountMemberAuthenticated } from "../../../lib/is-account-member-authenticated"; +import { isAccountMemberAuthenticated } from "src/lib/is-account-member-authenticated"; import { redirect } from "next/navigation"; -import Link from "next/link"; -import { Label } from "../../../components/label/Label"; -import { Input } from "../../../components/input/Input"; -import { FormStatusButton } from "../../../components/button/FormStatusButton"; +import { LocaleLink } from "src/components/LocaleLink"; +import { Label } from "src/components/label/Label"; +import { Input } from "src/components/input/Input"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; -export default async function Register() { +export default async function Register({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; const cookieStore = await cookies(); if (isAccountMemberAuthenticated(cookieStore)) { - redirect("/account/summary"); + redirect(lang ? `/${lang}/account/summary` : "/account/summary"); } return ( <>
- + - +

Register for an account

-
+ { + "use server" + await register(formData, lang as string) + }} + >
@@ -83,12 +90,12 @@ export default async function Register() {

Already a member?{" "} - Login now! - +

diff --git a/examples/core/src/app/(checkout)/checkout/AccountCheckout.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/AccountCheckout.tsx similarity index 84% rename from examples/core/src/app/(checkout)/checkout/AccountCheckout.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/AccountCheckout.tsx index 78abf139..f5d43e9e 100644 --- a/examples/core/src/app/(checkout)/checkout/AccountCheckout.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/AccountCheckout.tsx @@ -1,17 +1,17 @@ import { getSelectedAccount, retrieveAccountMemberCredentials, -} from "../../../lib/retrieve-account-member-credentials"; +} from "src/lib/retrieve-account-member-credentials"; import { cookies } from "next/headers"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../lib/cookie-constants"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { getV2AccountAddresses, getV2AccountMembersAccountMemberId, ResponseCurrency, } from "@epcc-sdk/sdks-shopper"; -import { createElasticPathClient } from "../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { getACart } from "@epcc-sdk/sdks-shopper"; -import { TAGS } from "../../../lib/constants"; +import { TAGS } from "src/lib/constants"; import { AccountCheckoutForm } from "./AccoutCheckoutForm"; export async function AccountCheckout({ diff --git a/examples/core/src/app/(checkout)/checkout/AccountDisplay.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/AccountDisplay.tsx similarity index 81% rename from examples/core/src/app/(checkout)/checkout/AccountDisplay.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/AccountDisplay.tsx index 5f4faf35..2fe1c99a 100644 --- a/examples/core/src/app/(checkout)/checkout/AccountDisplay.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/AccountDisplay.tsx @@ -1,20 +1,22 @@ "use client"; -import { Button } from "../../../components/button/Button"; -import { FormControl, FormField } from "../../../components/form/Form"; -import { Input } from "../../../components/input/Input"; +import { Button } from "src/components/button/Button"; +import { FormControl, FormField } from "src/components/form/Form"; +import { Input } from "src/components/input/Input"; import React, { useEffect, useTransition } from "react"; import { useFormContext } from "react-hook-form"; -import { CheckoutForm as CheckoutFormSchemaType } from "../../../components/checkout/form-schema/checkout-form-schema"; +import { CheckoutForm as CheckoutFormSchemaType } from "src/components/checkout/form-schema/checkout-form-schema"; import { logout } from "../../(auth)/actions"; -import { Skeleton } from "../../../components/skeleton/Skeleton"; +import { Skeleton } from "src/components/skeleton/Skeleton"; import { AccountMemberResponse } from "@epcc-sdk/sdks-shopper"; +import { useParams } from "next/navigation"; export function AccountDisplay({ accountMember, }: { accountMember: AccountMemberResponse; }) { + const { lang } = useParams(); const { control, setValue } = useFormContext(); const [_isPending, startTransition] = useTransition(); @@ -46,7 +48,7 @@ export function AccountDisplay({ diff --git a/examples/core/src/app/(checkout)/checkout/AccoutCheckoutForm.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/AccoutCheckoutForm.tsx similarity index 84% rename from examples/core/src/app/(checkout)/checkout/AccoutCheckoutForm.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/AccoutCheckoutForm.tsx index 56f8b8d0..22f80700 100644 --- a/examples/core/src/app/(checkout)/checkout/AccoutCheckoutForm.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/AccoutCheckoutForm.tsx @@ -1,6 +1,6 @@ -import Link from "next/link"; -import EpIcon from "../../../components/icons/ep-icon"; -import { Separator } from "../../../components/separator/Separator"; +import { LocaleLink } from "src/components/LocaleLink"; +import EpIcon from "src/components/icons/ep-icon"; +import { Separator } from "src/components/separator/Separator"; import { DeliveryForm } from "./DeliveryForm"; import { PaymentForm } from "./PaymentForm"; import { BillingForm } from "./BillingForm"; @@ -29,19 +29,19 @@ export function AccountCheckoutForm({ currencies: ResponseCurrency[]; }) { return ( - +
- + - +
- + - +
@@ -58,7 +58,7 @@ export function AccountCheckoutForm({
- {cart.data && } + {cart.data && }
diff --git a/examples/core/src/app/(checkout)/checkout/BillingForm.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/BillingForm.tsx similarity index 94% rename from examples/core/src/app/(checkout)/checkout/BillingForm.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/BillingForm.tsx index 8c1b95e4..e549506a 100644 --- a/examples/core/src/app/(checkout)/checkout/BillingForm.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/BillingForm.tsx @@ -1,17 +1,17 @@ "use client"; -import { CheckoutForm as CheckoutFormSchemaType } from "../../../components/checkout/form-schema/checkout-form-schema"; -import { Checkbox } from "../../../components/Checkbox"; +import { CheckoutForm as CheckoutFormSchemaType } from "src/components/checkout/form-schema/checkout-form-schema"; +import { Checkbox } from "src/components/Checkbox"; import { FormControl, FormField, FormItem, FormLabel, FormMessage, -} from "../../../components/form/Form"; +} from "src/components/form/Form"; import { useFormContext, useWatch } from "react-hook-form"; -import { Input } from "../../../components/input/Input"; +import { Input } from "src/components/input/Input"; import React, { useEffect } from "react"; -import { CountryCombobox } from "../../../components/combobox/CountryCombobox"; +import { CountryCombobox } from "src/components/combobox/CountryCombobox"; export function BillingForm() { const form = useFormContext(); diff --git a/examples/core/src/app/(checkout)/checkout/CheckoutFooter.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/CheckoutFooter.tsx similarity index 54% rename from examples/core/src/app/(checkout)/checkout/CheckoutFooter.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/CheckoutFooter.tsx index 9882b3a1..5dcb1426 100644 --- a/examples/core/src/app/(checkout)/checkout/CheckoutFooter.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/CheckoutFooter.tsx @@ -1,6 +1,6 @@ -import { Separator } from "../../../components/separator/Separator"; -import Link from "next/link"; -import EpLogo from "../../../components/icons/ep-logo"; +import { Separator } from "src/components/separator/Separator"; +import { LocaleLink } from "src/components/LocaleLink"; +import EpLogo from "src/components/icons/ep-logo"; import * as React from "react"; export function CheckoutFooter() { @@ -8,18 +8,18 @@ export function CheckoutFooter() {
- + Refund Policy - - + + Shipping Policy - - + + Privacy Policy - - + + Terms of Service - +
Powered by diff --git a/examples/core/src/app/(checkout)/checkout/CheckoutSidebar.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/CheckoutSidebar.tsx similarity index 88% rename from examples/core/src/app/(checkout)/checkout/CheckoutSidebar.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/CheckoutSidebar.tsx index d06b60ef..c3b85787 100644 --- a/examples/core/src/app/(checkout)/checkout/CheckoutSidebar.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/CheckoutSidebar.tsx @@ -1,25 +1,24 @@ "use client"; -import { Separator } from "../../../components/separator/Separator"; -import { CartDiscounts } from "../../../components/cart/CartDiscounts"; +import { Separator } from "src/components/separator/Separator"; +import { CartDiscounts } from "src/components/cart/CartDiscounts"; import * as React from "react"; import { ItemSidebarItems, ItemSidebarPromotions, ItemSidebarTotals, - ItemSidebarTotalsDiscount, - ItemSidebarTotalsSubTotal, ItemSidebarTotalsTax, resolveTotalInclShipping, -} from "../../../components/checkout-sidebar/ItemSidebar"; +} from "src/components/checkout-sidebar/ItemSidebar"; import { staticDeliveryMethods } from "./useShippingMethod"; -import { cn } from "../../../lib/cn"; +import { cn } from "src/lib/cn"; import { useWatch } from "react-hook-form"; -import { EP_CURRENCY_CODE } from "../../../lib/resolve-ep-currency-code"; -import { formatCurrency } from "../../../lib/format-currency"; -import { LoadingDots } from "../../../components/LoadingDots"; +import { formatCurrency } from "src/lib/format-currency"; +import { LoadingDots } from "src/components/LoadingDots"; import { getACart, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; -import { ItemSidebarHideable } from "../../../components/checkout-sidebar/ItemSidebarHideable"; -import { groupCartItems } from "../../../lib/group-cart-items"; +import { ItemSidebarHideable } from "src/components/checkout-sidebar/ItemSidebarHideable"; +import { groupCartItems } from "src/lib/group-cart-items"; +import { useParams } from "next/navigation"; +import { getPreferredCurrency } from "src/lib/i18n"; export function CheckoutSidebar({ cart, @@ -30,9 +29,9 @@ export function CheckoutSidebar({ }) { const shippingMethod = useWatch({ name: "shippingMethod" }); - const storeCurrency = currencies?.find( - (currency) => currency.code === EP_CURRENCY_CODE, - ); + const { lang } = useParams(); + const cartCurrencyCode = cart.data?.meta?.display_price?.with_tax?.currency; + const storeCurrency = getPreferredCurrency(lang as string, currencies, cartCurrencyCode); const groupedItems = groupCartItems(cart?.included?.items ?? []); const { regular, promotion, itemLevelPromotion, subscription_offerings } = groupedItems; @@ -105,7 +104,7 @@ export function CheckoutSidebar({ items={[...regular, ...subscription_offerings]} storeCurrency={storeCurrency} /> - + { if (!confirmationData && (cartResponse.included?.items?.length ?? 0) < 1) { - router.push("/cart"); + router.push(lang ? `/${lang}/cart` : "/cart"); } }, [cartResponse]); @@ -33,6 +34,7 @@ export function CheckoutViews({ ); } diff --git a/examples/core/src/app/(checkout)/checkout/ConfirmationSidebar.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/ConfirmationSidebar.tsx similarity index 82% rename from examples/core/src/app/(checkout)/checkout/ConfirmationSidebar.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/ConfirmationSidebar.tsx index 87670a54..d7d879a1 100644 --- a/examples/core/src/app/(checkout)/checkout/ConfirmationSidebar.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/ConfirmationSidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { Separator } from "../../../components/separator/Separator"; -import { CartDiscountsReadOnly } from "../../../components/cart/CartDiscounts"; +import { Separator } from "src/components/separator/Separator"; +import { CartDiscountsReadOnly } from "src/components/cart/CartDiscounts"; import * as React from "react"; import { ItemSidebarItems, @@ -9,19 +9,21 @@ import { ItemSidebarTotalsSubTotal, ItemSidebarTotalsTax, resolveTotalInclShipping, -} from "../../../components/checkout-sidebar/ItemSidebar"; +} from "src/components/checkout-sidebar/ItemSidebar"; import { staticDeliveryMethods } from "./useShippingMethod"; -import { EP_CURRENCY_CODE } from "../../../lib/resolve-ep-currency-code"; -import { LoadingDots } from "../../../components/LoadingDots"; -import { ItemSidebarHideable } from "../../../components/checkout-sidebar/ItemSidebarHideable"; -import { groupCartItems } from "../../../lib/group-cart-items"; +import { LoadingDots } from "src/components/LoadingDots"; +import { ItemSidebarHideable } from "src/components/checkout-sidebar/ItemSidebarHideable"; +import { groupCartItems } from "src/lib/group-cart-items"; import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import { useOrderConfirmation } from "./OrderConfirmationProvider"; +import { getPreferredCurrency } from "src/lib/i18n"; export function ConfirmationSidebar({ currencies, + lang, }: { currencies: ResponseCurrency[]; + lang: string; }) { const confirmationData = useOrderConfirmation(); @@ -43,9 +45,8 @@ export function ConfirmationSidebar({ method.value === shippingMethodCustomItem.sku, )?.amount; - const storeCurrency = currencies?.find( - (currency) => currency.code === EP_CURRENCY_CODE, - ); + const orderCurrencyCode = order.meta?.display_price?.with_tax?.currency; + const storeCurrency = getPreferredCurrency(lang as string, currencies, orderCurrencyCode); const formattedTotalAmountInclShipping = order.meta?.display_price?.with_tax?.amount !== undefined && diff --git a/examples/core/src/app/(checkout)/checkout/DeliveryForm.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/DeliveryForm.tsx similarity index 89% rename from examples/core/src/app/(checkout)/checkout/DeliveryForm.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/DeliveryForm.tsx index 9ba73ff5..18619847 100644 --- a/examples/core/src/app/(checkout)/checkout/DeliveryForm.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/DeliveryForm.tsx @@ -1,25 +1,25 @@ "use client"; -import { CheckoutForm as CheckoutFormSchemaType } from "../../../components/checkout/form-schema/checkout-form-schema"; +import { CheckoutForm as CheckoutFormSchemaType } from "src/components/checkout/form-schema/checkout-form-schema"; import { RadioGroup, RadioGroupItem, -} from "../../../components/radio-group/RadioGroup"; -import { Label } from "../../../components/label/Label"; -import { cn } from "../../../lib/cn"; +} from "src/components/radio-group/RadioGroup"; +import { Label } from "src/components/label/Label"; +import { cn } from "src/lib/cn"; import { FormControl, FormField, FormItem, FormLabel, FormMessage, -} from "../../../components/form/Form"; +} from "src/components/form/Form"; import { useFormContext } from "react-hook-form"; import { LightBulbIcon } from "@heroicons/react/24/outline"; import { Alert, AlertDescription, AlertTitle, -} from "../../../components/alert/Alert"; +} from "src/components/alert/Alert"; import { staticDeliveryMethods } from "./useShippingMethod"; export function DeliveryForm() { diff --git a/examples/core/src/app/(checkout)/checkout/GuestCheckout.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/GuestCheckout.tsx similarity index 84% rename from examples/core/src/app/(checkout)/checkout/GuestCheckout.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/GuestCheckout.tsx index f83084f1..783c22dd 100644 --- a/examples/core/src/app/(checkout)/checkout/GuestCheckout.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/GuestCheckout.tsx @@ -1,13 +1,13 @@ "use client"; import { GuestInformation } from "./GuestInformation"; import { ShippingForm } from "./ShippingForm"; -import Link from "next/link"; -import EpIcon from "../../../components/icons/ep-icon"; +import { LocaleLink } from "src/components/LocaleLink"; +import EpIcon from "src/components/icons/ep-icon"; import { DeliveryForm } from "./DeliveryForm"; import { PaymentForm } from "./PaymentForm"; import { BillingForm } from "./BillingForm"; import { SubmitCheckoutButton } from "./SubmitCheckoutButton"; -import { Separator } from "../../../components/separator/Separator"; +import { Separator } from "src/components/separator/Separator"; import * as React from "react"; import { CheckoutSidebar } from "./CheckoutSidebar"; import { getACart, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; @@ -25,20 +25,20 @@ export function GuestCheckout({ return item.type === "subscription_item"; }) ?? false; return ( - +
- + - +
- + - +
@@ -53,7 +53,7 @@ export function GuestCheckout({
- {cart?.data && } + {cart?.data && }
diff --git a/examples/core/src/app/(checkout)/checkout/GuestInformation.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/GuestInformation.tsx similarity index 89% rename from examples/core/src/app/(checkout)/checkout/GuestInformation.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/GuestInformation.tsx index 92009e61..bd36f901 100644 --- a/examples/core/src/app/(checkout)/checkout/GuestInformation.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/GuestInformation.tsx @@ -3,8 +3,8 @@ import { AnonymousCheckoutForm, SubscriptionCheckoutForm, -} from "../../../components/checkout/form-schema/checkout-form-schema"; -import Link from "next/link"; +} from "src/components/checkout/form-schema/checkout-form-schema"; +import { LocaleLink } from "src/components/LocaleLink"; import { usePathname } from "next/navigation"; import { FormControl, @@ -13,11 +13,11 @@ import { FormItem, FormLabel, FormMessage, -} from "../../../components/form/Form"; -import { Input } from "../../../components/input/Input"; +} from "src/components/form/Form"; +import { Input } from "src/components/input/Input"; import React from "react"; import { useFormContext, useWatch } from "react-hook-form"; -import { Checkbox } from "../../../components/Checkbox"; +import { Checkbox } from "src/components/Checkbox"; export function GuestInformation() { const pathname = usePathname(); @@ -35,12 +35,12 @@ export function GuestInformation() { Your Info Already a customer?{" "} - Sign in - +
diff --git a/examples/core/src/app/(checkout)/checkout/OrderConfirmation.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/OrderConfirmation.tsx similarity index 85% rename from examples/core/src/app/(checkout)/checkout/OrderConfirmation.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/OrderConfirmation.tsx index 959ba3c5..dd9a5e76 100644 --- a/examples/core/src/app/(checkout)/checkout/OrderConfirmation.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/OrderConfirmation.tsx @@ -1,20 +1,22 @@ "use client"; import { ConfirmationSidebar } from "./ConfirmationSidebar"; -import Link from "next/link"; -import EpIcon from "../../../components/icons/ep-icon"; +import { LocaleLink } from "src/components/LocaleLink"; +import EpIcon from "src/components/icons/ep-icon"; import * as React from "react"; -import { Separator } from "../../../components/separator/Separator"; +import { Separator } from "src/components/separator/Separator"; import { CheckoutFooter } from "./CheckoutFooter"; -import { Button } from "../../../components/button/Button"; +import { Button } from "src/components/button/Button"; import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import { PaymentCompleteResponse } from "./actions"; export function OrderConfirmation({ currencies, confirmationData, + lang, }: { currencies: ResponseCurrency[]; confirmationData: PaymentCompleteResponse; + lang: string; }) { if (!confirmationData) { return null; @@ -33,17 +35,17 @@ export function OrderConfirmation({ return (
- + - +
{/* Confirmation Content */}
- + - +
@@ -51,7 +53,7 @@ export function OrderConfirmation({
@@ -90,7 +92,7 @@ export function OrderConfirmation({
{`Order #${orderId}`} - +
diff --git a/examples/core/src/app/(checkout)/checkout/OrderConfirmationProvider.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/OrderConfirmationProvider.tsx similarity index 100% rename from examples/core/src/app/(checkout)/checkout/OrderConfirmationProvider.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/OrderConfirmationProvider.tsx diff --git a/examples/core/src/app/(checkout)/checkout/PaymentForm.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/PaymentForm.tsx similarity index 95% rename from examples/core/src/app/(checkout)/checkout/PaymentForm.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/PaymentForm.tsx index ad609d01..e8002516 100644 --- a/examples/core/src/app/(checkout)/checkout/PaymentForm.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/PaymentForm.tsx @@ -4,7 +4,7 @@ import { Alert, AlertDescription, AlertTitle, -} from "../../../components/alert/Alert"; +} from "src/components/alert/Alert"; export function PaymentForm() { return ( diff --git a/examples/core/src/app/(checkout)/checkout/ShippingForm.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/ShippingForm.tsx similarity index 95% rename from examples/core/src/app/(checkout)/checkout/ShippingForm.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/ShippingForm.tsx index bbef2821..3ae77d9a 100644 --- a/examples/core/src/app/(checkout)/checkout/ShippingForm.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/ShippingForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { CheckoutForm as CheckoutFormSchemaType } from "../../../components/checkout/form-schema/checkout-form-schema"; +import { CheckoutForm as CheckoutFormSchemaType } from "src/components/checkout/form-schema/checkout-form-schema"; import { useFormContext } from "react-hook-form"; import { FormControl, @@ -8,10 +8,10 @@ import { FormItem, FormLabel, FormMessage, -} from "../../../components/form/Form"; -import { Input } from "../../../components/input/Input"; +} from "src/components/form/Form"; +import { Input } from "src/components/input/Input"; import React from "react"; -import { CountryCombobox } from "../../../components/combobox/CountryCombobox"; +import { CountryCombobox } from "src/components/combobox/CountryCombobox"; export function ShippingForm() { const form = useFormContext(); diff --git a/examples/core/src/app/(checkout)/checkout/ShippingSelector.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/ShippingSelector.tsx similarity index 85% rename from examples/core/src/app/(checkout)/checkout/ShippingSelector.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/ShippingSelector.tsx index c3731df2..4e0368bb 100644 --- a/examples/core/src/app/(checkout)/checkout/ShippingSelector.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/ShippingSelector.tsx @@ -5,13 +5,13 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "../../../components/select/Select"; +} from "src/components/select/Select"; import { useFormContext } from "react-hook-form"; -import { CheckoutForm as CheckoutFormSchemaType } from "../../../components/checkout/form-schema/checkout-form-schema"; +import { CheckoutForm as CheckoutFormSchemaType } from "src/components/checkout/form-schema/checkout-form-schema"; import { useEffect } from "react"; -import { Skeleton } from "../../../components/skeleton/Skeleton"; -import { Button } from "../../../components/button/Button"; -import Link from "next/link"; +import { Skeleton } from "src/components/skeleton/Skeleton"; +import { Button } from "src/components/button/Button"; +import { LocaleLink } from "src/components/LocaleLink"; import { AccountAddressResponse } from "@epcc-sdk/sdks-shopper"; export function ShippingSelector({ @@ -80,7 +80,7 @@ export function ShippingSelector({ className="text-base font-normal" type="button" > - Add new... + Add new... diff --git a/examples/core/src/app/[lang]/(checkout)/checkout/SubmitCheckoutButton.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/SubmitCheckoutButton.tsx new file mode 100644 index 00000000..6c4770c6 --- /dev/null +++ b/examples/core/src/app/[lang]/(checkout)/checkout/SubmitCheckoutButton.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useCheckout } from "./checkout-provider"; +import { StatusButton } from "src/components/button/StatusButton"; +import { CartResponse, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; +import { staticDeliveryMethods } from "./useShippingMethod"; +import { useWatch } from "react-hook-form"; +import { useParams } from "next/navigation"; +import { getPreferredCurrency } from "src/lib/i18n"; +import { resolveTotalInclShipping } from "src/components/checkout-sidebar/ItemSidebar"; + +export function SubmitCheckoutButton({ cart, currencies }: { cart: CartResponse; currencies: ResponseCurrency[]; }) { + const { handleSubmit, completePayment, isCompleting } = useCheckout(); + + const { lang } = useParams(); + const cartCurrencyCode = cart.meta?.display_price?.with_tax?.currency; + const storeCurrency = getPreferredCurrency(lang as string, currencies, cartCurrencyCode); + + const shippingMethod = useWatch({ name: "shippingMethod" }); + const shippingAmount = staticDeliveryMethods.find( + (method) => method.value === shippingMethod, + )?.amount; + + const formattedTotalAmountInclShipping = + cart.meta?.display_price?.with_tax?.amount !== undefined && + shippingAmount !== undefined && + storeCurrency + ? resolveTotalInclShipping( + shippingAmount, + cart.meta.display_price.with_tax.amount, + storeCurrency, + ) + : cart.meta?.display_price?.with_tax?.formatted; + + return ( + { + return completePayment(values); + })} + > + {`Pay ${formattedTotalAmountInclShipping}`} + + ); +} diff --git a/examples/core/src/app/(checkout)/checkout/actions.ts b/examples/core/src/app/[lang]/(checkout)/checkout/actions.ts similarity index 84% rename from examples/core/src/app/(checkout)/checkout/actions.ts rename to examples/core/src/app/[lang]/(checkout)/checkout/actions.ts index 8d04636f..845a94fa 100644 --- a/examples/core/src/app/(checkout)/checkout/actions.ts +++ b/examples/core/src/app/[lang]/(checkout)/checkout/actions.ts @@ -5,8 +5,8 @@ import { staticDeliveryMethods } from "./useShippingMethod"; import { CheckoutForm, checkoutFormSchema, -} from "../../../components/checkout/form-schema/checkout-form-schema"; -import { createElasticPathClient } from "../../../lib/create-elastic-path-client"; +} from "src/components/checkout/form-schema/checkout-form-schema"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { manageCarts, checkoutApi, @@ -23,18 +23,18 @@ import { postV2AccountMembersTokens, CartsResponse, } from "@epcc-sdk/sdks-shopper"; -import { getCartCookieServer } from "../../../lib/cart-cookie-server"; +import { getCartCookieServer } from "src/lib/cart-cookie-server"; import { cookies } from "next/headers"; import { getSelectedAccount, retrieveAccountMemberCredentials, -} from "../../../lib/retrieve-account-member-credentials"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../lib/cookie-constants"; +} from "src/lib/retrieve-account-member-credentials"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { revalidatePath, revalidateTag } from "next/cache"; -import { extractCartItemProductIds } from "../../../lib/extract-cart-item-product-ids"; +import { extractCartItemProductIds } from "src/lib/extract-cart-item-product-ids"; import { extractCartItemMedia } from "./extract-cart-item-media"; -import { generatePassword } from "../../../lib/generate-password"; -import { createCookieFromGenerateTokenResponse } from "../../../lib/create-cookie-from-generate-token-response"; +import { generatePassword } from "src/lib/generate-password"; +import { createCookieFromGenerateTokenResponse } from "src/lib/create-cookie-from-generate-token-response"; const PASSWORD_PROFILE_ID = process.env.NEXT_PUBLIC_PASSWORD_PROFILE_ID!; @@ -48,6 +48,8 @@ export type PaymentCompleteResponse = { export async function paymentComplete( props: CheckoutForm, + lang: string, + currencyCode?: string, ): Promise { const client = createElasticPathClient(); @@ -99,6 +101,10 @@ export async function paymentComplete( }, }, }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); if (!cartInclShippingResponse.data?.data) { @@ -162,7 +168,7 @@ export async function paymentComplete( ); if (!accountMemberCredentials) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } const selectedAccount = getSelectedAccount(accountMemberCredentials); @@ -192,6 +198,10 @@ export async function paymentComplete( shipping_address: checkoutProps.shippingAddress as ShippingAddress, }, }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); } else { createdOrderResonse = await checkoutApi({ @@ -209,6 +219,10 @@ export async function paymentComplete( shipping_address: checkoutProps.shippingAddress as ShippingAddress, }, }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); } @@ -231,6 +245,10 @@ export async function paymentComplete( method: "purchase", }, }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); /** @@ -245,6 +263,10 @@ export async function paymentComplete( query: { include: ["items"], }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); const items = cartResponse.data?.included?.items ?? []; @@ -257,6 +279,10 @@ export async function paymentComplete( filter: `in(id,${productIds})`, include: ["main_image"], }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); const images = extractCartItemMedia({ @@ -273,6 +299,10 @@ export async function paymentComplete( path: { cartID: cartId, }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyCode, + }, }); const revalidatePromises = [revalidatePath("/cart"), revalidateTag("cart")]; diff --git a/examples/core/src/app/(checkout)/checkout/checkout-provider.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/checkout-provider.tsx similarity index 81% rename from examples/core/src/app/(checkout)/checkout/checkout-provider.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/checkout-provider.tsx index 29183178..20a0aec1 100644 --- a/examples/core/src/app/(checkout)/checkout/checkout-provider.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/checkout-provider.tsx @@ -8,13 +8,15 @@ import { CheckoutForm, NonAccountCheckoutForm, nonAccountCheckoutFormSchema, -} from "../../../components/checkout/form-schema/checkout-form-schema"; +} from "src/components/checkout/form-schema/checkout-form-schema"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Form } from "../../../components/form/Form"; +import { Form } from "src/components/form/Form"; import { ShippingMethod, staticDeliveryMethods } from "./useShippingMethod"; -import { getACart } from "@epcc-sdk/sdks-shopper"; +import { getACart, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import { paymentComplete } from "./actions"; import { useSetOrderConfirmation } from "./OrderConfirmationProvider"; +import { useParams } from "next/navigation"; +import { getPreferredCurrency } from "src/lib/i18n"; type CheckoutContext = { cart?: NonNullable>["data"]>; @@ -30,6 +32,8 @@ const CheckoutContext = createContext(null); type CheckoutProviderProps = { children?: React.ReactNode; type: "subscription" | "guest"; + cart?: NonNullable>["data"]>; + currencies?: ResponseCurrency[]; }; const guestFormDefaults = { @@ -113,7 +117,12 @@ const accountFormDefaults = { export function GuestCheckoutProvider({ children, type, + cart, + currencies = [], }: CheckoutProviderProps) { + const { lang } = useParams(); + const cartCurrencyCode = cart?.data?.meta?.display_price?.with_tax?.currency; + const storeCurrency = getPreferredCurrency(lang as string, currencies, cartCurrencyCode); const [isPending, startTransition] = useTransition(); const setConfirmationData = useSetOrderConfirmation(); @@ -125,7 +134,7 @@ export function GuestCheckoutProvider({ async function handleSubmit(data: CheckoutForm) { startTransition(async () => { - const result = await paymentComplete(data); + const result = await paymentComplete(data, lang as string, storeCurrency?.code); setConfirmationData(result); }); } @@ -149,7 +158,12 @@ export function GuestCheckoutProvider({ export function AccountCheckoutProvider({ children, + cart, + currencies = [], }: Omit) { + const { lang } = useParams(); + const cartCurrencyCode = cart?.data?.meta?.display_price?.with_tax?.currency; + const storeCurrency = getPreferredCurrency(lang as string, currencies, cartCurrencyCode); const [isPending, startTransition] = useTransition(); const setConfirmationData = useSetOrderConfirmation(); @@ -160,7 +174,7 @@ export function AccountCheckoutProvider({ async function handleSubmit(data: CheckoutForm) { startTransition(async () => { - const result = await paymentComplete(data); + const result = await paymentComplete(data, lang as string, storeCurrency?.code); setConfirmationData(result); }); } diff --git a/examples/core/src/app/(checkout)/checkout/extract-cart-item-media.ts b/examples/core/src/app/[lang]/(checkout)/checkout/extract-cart-item-media.ts similarity index 100% rename from examples/core/src/app/(checkout)/checkout/extract-cart-item-media.ts rename to examples/core/src/app/[lang]/(checkout)/checkout/extract-cart-item-media.ts diff --git a/examples/core/src/app/(checkout)/checkout/page.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/page.tsx similarity index 74% rename from examples/core/src/app/(checkout)/checkout/page.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/page.tsx index 9eed768e..146b85e7 100644 --- a/examples/core/src/app/(checkout)/checkout/page.tsx +++ b/examples/core/src/app/[lang]/(checkout)/checkout/page.tsx @@ -1,27 +1,37 @@ import { Metadata } from "next"; import { AccountCheckout } from "./AccountCheckout"; -import { CART_COOKIE_NAME } from "../../../lib/cookie-constants"; +import { CART_COOKIE_NAME } from "src/lib/cookie-constants"; import { GuestCheckout } from "./GuestCheckout"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { CheckoutViews } from "./CheckoutViews"; import { getAllCurrencies, getACart, getByContextProduct } from "@epcc-sdk/sdks-shopper"; -import { createElasticPathClient } from "../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { OrderConfirmationProvider } from "./OrderConfirmationProvider"; -import { TAGS } from "../../../lib/constants"; +import { TAGS } from "src/lib/constants"; import { isAccountAuthenticated } from "@epcc-sdk/sdks-nextjs"; +import { getPreferredCurrency } from "src/lib/i18n"; export const metadata: Metadata = { title: "Checkout", }; -export default async function CheckoutPage() { +export default async function CheckoutPage({ params }: { params: Promise<{ lang: string }> }) { const cartCookie = (await cookies()).get(CART_COOKIE_NAME); const client = createElasticPathClient(); + const { lang } = await params; if (!cartCookie) { throw new Error("Cart cookie not found"); } + const currencies = await getAllCurrencies({ + client, + next: { + tags: [TAGS.currencies], + }, + }); + const currency = getPreferredCurrency(lang, currencies.data?.data || []); + const cartResponse = await getACart({ client, path: { @@ -33,14 +43,25 @@ export default async function CheckoutPage() { next: { tags: [TAGS.cart], }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currency?.code, + } }); + const cartCurrency = cartResponse.data?.data?.meta?.display_price?.with_tax?.currency; + const currencyUpdated = getPreferredCurrency(lang, currencies.data?.data || [], cartCurrency); + // Fetch product details for each cart item to get original sale price const cartItems = cartResponse?.data?.included?.items; const productDetailsPromises = cartItems?.map(item => getByContextProduct({ client, path: { product_id: item.product_id! }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyUpdated?.code, + } }) ); const productDetails = await Promise.all(productDetailsPromises || []); @@ -59,13 +80,6 @@ export default async function CheckoutPage() { notFound(); } - const currencies = await getAllCurrencies({ - client, - next: { - tags: [TAGS.currencies], - }, - }); - const isAccount = await isAccountAuthenticated(); return ( diff --git a/examples/core/src/app/(checkout)/checkout/useShippingMethod.tsx b/examples/core/src/app/[lang]/(checkout)/checkout/useShippingMethod.tsx similarity index 100% rename from examples/core/src/app/(checkout)/checkout/useShippingMethod.tsx rename to examples/core/src/app/[lang]/(checkout)/checkout/useShippingMethod.tsx diff --git a/examples/core/src/app/(checkout)/layout.tsx b/examples/core/src/app/[lang]/(checkout)/layout.tsx similarity index 85% rename from examples/core/src/app/(checkout)/layout.tsx rename to examples/core/src/app/[lang]/(checkout)/layout.tsx index c311381e..9de4e672 100644 --- a/examples/core/src/app/(checkout)/layout.tsx +++ b/examples/core/src/app/[lang]/(checkout)/layout.tsx @@ -1,9 +1,9 @@ import localFont from "next/font/local"; import { ReactNode } from "react"; -import { getStoreInitialState } from "../../lib/get-store-initial-state"; +import { getStoreInitialState } from "src/lib/get-store-initial-state"; import { Providers } from "../providers"; import clsx from "clsx"; -import { createElasticPathClient } from "../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME; const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL @@ -23,7 +23,7 @@ export const metadata = { }; const inter = localFont({ - src: "../../../public/fonts/Inter-VariableFont_opsz,wght.ttf", + src: "../../../../public/fonts/Inter-VariableFont_opsz,wght.ttf", display: "swap", variable: "--font-inter", }); diff --git a/examples/core/src/app/(checkout)/not-found.tsx b/examples/core/src/app/[lang]/(checkout)/not-found.tsx similarity index 64% rename from examples/core/src/app/(checkout)/not-found.tsx rename to examples/core/src/app/[lang]/(checkout)/not-found.tsx index d97d88f4..b568b0d7 100644 --- a/examples/core/src/app/(checkout)/not-found.tsx +++ b/examples/core/src/app/[lang]/(checkout)/not-found.tsx @@ -1,13 +1,13 @@ -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; export default function NotFound() { return (
404 - The page could not be found. - + Back to home - +
); } diff --git a/examples/core/src/app/(store)/ClientProvider.tsx b/examples/core/src/app/[lang]/(store)/ClientProvider.tsx similarity index 100% rename from examples/core/src/app/(store)/ClientProvider.tsx rename to examples/core/src/app/[lang]/(store)/ClientProvider.tsx diff --git a/examples/core/src/app/(store)/StoreProvider.tsx b/examples/core/src/app/[lang]/(store)/StoreProvider.tsx similarity index 83% rename from examples/core/src/app/(store)/StoreProvider.tsx rename to examples/core/src/app/[lang]/(store)/StoreProvider.tsx index a789a1e5..389f7360 100644 --- a/examples/core/src/app/(store)/StoreProvider.tsx +++ b/examples/core/src/app/[lang]/(store)/StoreProvider.tsx @@ -1,8 +1,8 @@ "use client"; import React, { createContext, ReactNode, useContext } from "react"; -import { InitialState } from "../../lib/get-store-initial-state"; -import { NavigationNode } from "../../lib/build-site-navigation"; +import { InitialState } from "src/lib/get-store-initial-state"; +import { NavigationNode } from "src/lib/build-site-navigation"; interface StoreState { nav?: NavigationNode[]; diff --git a/examples/core/src/app/(store)/about/page.tsx b/examples/core/src/app/[lang]/(store)/about/page.tsx similarity index 56% rename from examples/core/src/app/(store)/about/page.tsx rename to examples/core/src/app/[lang]/(store)/about/page.tsx index fc900ed0..6935aac9 100644 --- a/examples/core/src/app/(store)/about/page.tsx +++ b/examples/core/src/app/[lang]/(store)/about/page.tsx @@ -1,4 +1,4 @@ -import Blurb from "../../../components/shared/blurb"; +import Blurb from "src/components/shared/blurb"; export default function About() { return ; diff --git a/examples/core/src/app/(store)/account/AccountNavigation.tsx b/examples/core/src/app/[lang]/(store)/account/AccountNavigation.tsx similarity index 70% rename from examples/core/src/app/(store)/account/AccountNavigation.tsx rename to examples/core/src/app/[lang]/(store)/account/AccountNavigation.tsx index 15324a86..ae7cdfcc 100644 --- a/examples/core/src/app/(store)/account/AccountNavigation.tsx +++ b/examples/core/src/app/[lang]/(store)/account/AccountNavigation.tsx @@ -1,11 +1,12 @@ "use client"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { Button } from "../../../components/button/Button"; +import { LocaleLink } from "src/components/LocaleLink"; +import { useParams, usePathname } from "next/navigation"; +import { Button } from "src/components/button/Button"; import { logout } from "../../(auth)/actions"; import { useTransition } from "react"; export function AccountNavigation() { + const { lang } = useParams(); const pathname = usePathname(); const [_isPending, startTransition] = useTransition(); @@ -18,7 +19,7 @@ export function AccountNavigation() { asChild reversed={!pathname.startsWith("/account/summary")} > - Account Info + Account Info
  • @@ -27,7 +28,7 @@ export function AccountNavigation() { asChild reversed={!pathname.startsWith("/account/orders")} > - My Orders + My Orders
  • @@ -36,14 +37,14 @@ export function AccountNavigation() { asChild reversed={!pathname.startsWith("/account/addresses")} > - Addresses + Addresses
  • diff --git a/examples/core/src/app/(store)/account/addresses/DeleteAddressBtn.tsx b/examples/core/src/app/[lang]/(store)/account/addresses/DeleteAddressBtn.tsx similarity index 88% rename from examples/core/src/app/(store)/account/addresses/DeleteAddressBtn.tsx rename to examples/core/src/app/[lang]/(store)/account/addresses/DeleteAddressBtn.tsx index 6f255b7c..585fe967 100644 --- a/examples/core/src/app/(store)/account/addresses/DeleteAddressBtn.tsx +++ b/examples/core/src/app/[lang]/(store)/account/addresses/DeleteAddressBtn.tsx @@ -1,7 +1,7 @@ "use client"; import { deleteAddress } from "./actions"; -import { FormStatusButton } from "../../../../components/button/FormStatusButton"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; import { TrashIcon } from "@heroicons/react/24/outline"; import React from "react"; diff --git a/examples/core/src/app/(store)/account/addresses/[addressId]/UpdateForm.tsx b/examples/core/src/app/[lang]/(store)/account/addresses/[addressId]/UpdateForm.tsx similarity index 94% rename from examples/core/src/app/(store)/account/addresses/[addressId]/UpdateForm.tsx rename to examples/core/src/app/[lang]/(store)/account/addresses/[addressId]/UpdateForm.tsx index 5ceb32be..49710935 100644 --- a/examples/core/src/app/(store)/account/addresses/[addressId]/UpdateForm.tsx +++ b/examples/core/src/app/[lang]/(store)/account/addresses/[addressId]/UpdateForm.tsx @@ -1,16 +1,16 @@ import { updateAddress } from "../actions"; -import { Label } from "../../../../../components/label/Label"; -import { Input } from "../../../../../components/input/Input"; +import { Label } from "src/components/label/Label"; +import { Input } from "src/components/input/Input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "../../../../../components/select/Select"; -import { FormStatusButton } from "../../../../../components/button/FormStatusButton"; +} from "src/components/select/Select"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; import React from "react"; -import { countries as staticCountries } from "../../../../../lib/all-countries"; +import { countries as staticCountries } from "src/lib/all-countries"; import { AccountAddressResponse } from "@epcc-sdk/sdks-shopper"; export function UpdateForm({ diff --git a/examples/core/src/app/(store)/account/addresses/[addressId]/page.tsx b/examples/core/src/app/[lang]/(store)/account/addresses/[addressId]/page.tsx similarity index 72% rename from examples/core/src/app/(store)/account/addresses/[addressId]/page.tsx rename to examples/core/src/app/[lang]/(store)/account/addresses/[addressId]/page.tsx index d8c3dbc3..4d51b1ad 100644 --- a/examples/core/src/app/(store)/account/addresses/[addressId]/page.tsx +++ b/examples/core/src/app/[lang]/(store)/account/addresses/[addressId]/page.tsx @@ -3,24 +3,25 @@ import { notFound, redirect } from "next/navigation"; import { getSelectedAccount, retrieveAccountMemberCredentials, -} from "../../../../../lib/retrieve-account-member-credentials"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../../lib/cookie-constants"; -import { Button } from "../../../../../components/button/Button"; -import Link from "next/link"; +} from "src/lib/retrieve-account-member-credentials"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; +import { Button } from "src/components/button/Button"; +import { LocaleLink } from "src/components/LocaleLink"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import React from "react"; -import { Separator } from "../../../../../components/separator/Separator"; +import { Separator } from "src/components/separator/Separator"; import { UpdateForm } from "./UpdateForm"; -import { createElasticPathClient } from "../../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { getV2AccountAddress } from "@epcc-sdk/sdks-shopper"; export const dynamic = "force-dynamic"; export default async function Address(props: { - params: Promise<{ addressId: string }>; + params: Promise<{ addressId: string, lang: string }>; }) { const params = await props.params; const cookieStore = await cookies(); + const lang = params?.lang; const accountMemberCookie = retrieveAccountMemberCredentials( cookieStore, @@ -28,7 +29,7 @@ export default async function Address(props: { ); if (!accountMemberCookie) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } const { addressId } = params; @@ -54,10 +55,10 @@ export default async function Address(props: {
    diff --git a/examples/core/src/app/(store)/account/addresses/actions.ts b/examples/core/src/app/[lang]/(store)/account/addresses/actions.ts similarity index 91% rename from examples/core/src/app/(store)/account/addresses/actions.ts rename to examples/core/src/app/[lang]/(store)/account/addresses/actions.ts index 45fa86b7..8df65901 100644 --- a/examples/core/src/app/(store)/account/addresses/actions.ts +++ b/examples/core/src/app/[lang]/(store)/account/addresses/actions.ts @@ -5,12 +5,12 @@ import { cookies } from "next/headers"; import { getSelectedAccount, retrieveAccountMemberCredentials, -} from "../../../../lib/retrieve-account-member-credentials"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../lib/cookie-constants"; +} from "src/lib/retrieve-account-member-credentials"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { revalidatePath, revalidateTag } from "next/cache"; -import { shippingAddressSchema } from "../../../../components/checkout/form-schema/checkout-form-schema"; +import { shippingAddressSchema } from "src/components/checkout/form-schema/checkout-form-schema"; import { redirect } from "next/navigation"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { deleteV2AccountAddress, postV2AccountAddress, @@ -138,7 +138,7 @@ export async function updateAddress(formData: FormData) { } } -export async function addAddress(formData: FormData) { +export async function addAddress(formData: FormData, lang?: string) { const client = await createElasticPathClient(); const rawEntries = Object.fromEntries(formData.entries()); @@ -196,5 +196,5 @@ export async function addAddress(formData: FormData) { await Promise.all(revalidatePromises); - redirect(redirectUrl); + redirect(lang ? `/${lang}${redirectUrl}`: redirectUrl); } diff --git a/examples/core/src/app/(store)/account/addresses/add/AddForm.tsx b/examples/core/src/app/[lang]/(store)/account/addresses/add/AddForm.tsx similarity index 92% rename from examples/core/src/app/(store)/account/addresses/add/AddForm.tsx rename to examples/core/src/app/[lang]/(store)/account/addresses/add/AddForm.tsx index ef23a7b0..7c2cfdc7 100644 --- a/examples/core/src/app/(store)/account/addresses/add/AddForm.tsx +++ b/examples/core/src/app/[lang]/(store)/account/addresses/add/AddForm.tsx @@ -1,25 +1,25 @@ import { addAddress } from "../actions"; -import { Label } from "../../../../../components/label/Label"; -import { Input } from "../../../../../components/input/Input"; +import { Label } from "src/components/label/Label"; +import { Input } from "src/components/input/Input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "../../../../../components/select/Select"; -import { FormStatusButton } from "../../../../../components/button/FormStatusButton"; +} from "src/components/select/Select"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; import React from "react"; -import { countries as staticCountries } from "../../../../../lib/all-countries"; +import { countries as staticCountries } from "src/lib/all-countries"; -export function AddForm() { +export function AddForm({ lang }: { lang: string }) { const countries = staticCountries; return (
    { "use server"; - await addAddress(formData); + await addAddress(formData, lang); }} className="flex flex-col gap-5" > diff --git a/examples/core/src/app/(store)/account/addresses/add/page.tsx b/examples/core/src/app/[lang]/(store)/account/addresses/add/page.tsx similarity index 57% rename from examples/core/src/app/(store)/account/addresses/add/page.tsx rename to examples/core/src/app/[lang]/(store)/account/addresses/add/page.tsx index 12641f83..7ef16fab 100644 --- a/examples/core/src/app/(store)/account/addresses/add/page.tsx +++ b/examples/core/src/app/[lang]/(store)/account/addresses/add/page.tsx @@ -1,17 +1,19 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { retrieveAccountMemberCredentials } from "../../../../../lib/retrieve-account-member-credentials"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../../lib/cookie-constants"; -import { Button } from "../../../../../components/button/Button"; -import Link from "next/link"; +import { retrieveAccountMemberCredentials } from "src/lib/retrieve-account-member-credentials"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; +import { Button } from "src/components/button/Button"; +import { LocaleLink } from "src/components/LocaleLink"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import React from "react"; -import { Separator } from "../../../../../components/separator/Separator"; +import { Separator } from "src/components/separator/Separator"; import { AddForm } from "./AddForm"; export const dynamic = "force-dynamic"; -export default async function AddAddress() { +export default async function AddAddress({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; + const cookieStore = await cookies(); const accountMemberCookie = retrieveAccountMemberCredentials( @@ -20,23 +22,23 @@ export default async function AddAddress() { ); if (!accountMemberCookie) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } return (

    Add Address

    - +
    ); diff --git a/examples/core/src/app/(store)/account/addresses/page.tsx b/examples/core/src/app/[lang]/(store)/account/addresses/page.tsx similarity index 77% rename from examples/core/src/app/(store)/account/addresses/page.tsx rename to examples/core/src/app/[lang]/(store)/account/addresses/page.tsx index 30939238..ca51fa43 100644 --- a/examples/core/src/app/(store)/account/addresses/page.tsx +++ b/examples/core/src/app/[lang]/(store)/account/addresses/page.tsx @@ -1,23 +1,25 @@ import { PencilSquareIcon, PlusIcon } from "@heroicons/react/24/outline"; import { cookies } from "next/headers"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../lib/cookie-constants"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { redirect } from "next/navigation"; import { getSelectedAccount, retrieveAccountMemberCredentials, -} from "../../../../lib/retrieve-account-member-credentials"; -import Link from "next/link"; -import { Button } from "../../../../components/button/Button"; -import { Separator } from "../../../../components/separator/Separator"; +} from "src/lib/retrieve-account-member-credentials"; +import { LocaleLink } from "src/components/LocaleLink"; +import { Button } from "src/components/button/Button"; +import { Separator } from "src/components/separator/Separator"; import React from "react"; import { DeleteAddressBtn } from "./DeleteAddressBtn"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { getV2AccountAddresses } from "@epcc-sdk/sdks-shopper"; -import { TAGS } from "../../../../lib/constants"; +import { TAGS } from "src/lib/constants"; export const dynamic = "force-dynamic"; -export default async function Addresses() { + +export default async function Addresses({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; const cookieStore = await cookies(); const accountMemberCookie = retrieveAccountMemberCredentials( @@ -26,7 +28,7 @@ export default async function Addresses() { ); if (!accountMemberCookie) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } const selectedAccount = getSelectedAccount(accountMemberCookie); @@ -69,10 +71,10 @@ export default async function Addresses() {
    @@ -83,10 +85,10 @@ export default async function Addresses() {
  • diff --git a/examples/core/src/app/(store)/account/layout.tsx b/examples/core/src/app/[lang]/(store)/account/layout.tsx similarity index 100% rename from examples/core/src/app/(store)/account/layout.tsx rename to examples/core/src/app/[lang]/(store)/account/layout.tsx diff --git a/examples/core/src/app/(store)/account/orders/OrderItem.tsx b/examples/core/src/app/[lang]/(store)/account/orders/OrderItem.tsx similarity index 88% rename from examples/core/src/app/(store)/account/orders/OrderItem.tsx rename to examples/core/src/app/[lang]/(store)/account/orders/OrderItem.tsx index 0324a301..abea298f 100644 --- a/examples/core/src/app/(store)/account/orders/OrderItem.tsx +++ b/examples/core/src/app/[lang]/(store)/account/orders/OrderItem.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; import { ProductThumbnail } from "./[orderId]/ProductThumbnail"; -import Link from "next/link"; -import { formatIsoDateString } from "../../../../lib/format-iso-date-string"; +import { LocaleLink } from "src/components/LocaleLink"; +import { formatIsoDateString } from "src/lib/format-iso-date-string"; import { OrderItemResponse, OrderResponse } from "@epcc-sdk/sdks-shopper"; export type OrderItemProps = { @@ -29,22 +29,22 @@ export function OrderItem({ return (
    - + - +
    Order # {order.external_ref ?? order.id} - +

    {formatOrderItemsTitle(sortedOrderItems)}

    - +
    {children}
    diff --git a/examples/core/src/app/(store)/account/orders/OrderItemWithDetails.tsx b/examples/core/src/app/[lang]/(store)/account/orders/OrderItemWithDetails.tsx similarity index 92% rename from examples/core/src/app/(store)/account/orders/OrderItemWithDetails.tsx rename to examples/core/src/app/[lang]/(store)/account/orders/OrderItemWithDetails.tsx index 03bffaa0..a65ba3a7 100644 --- a/examples/core/src/app/(store)/account/orders/OrderItemWithDetails.tsx +++ b/examples/core/src/app/[lang]/(store)/account/orders/OrderItemWithDetails.tsx @@ -1,5 +1,5 @@ import { OrderItem, OrderItemProps, sortOrderItems } from "./OrderItem"; -import { formatIsoDateString } from "../../../../lib/format-iso-date-string"; +import { formatIsoDateString } from "src/lib/format-iso-date-string"; export function OrderItemWithDetails(props: Omit) { const sortedOrderItems = sortOrderItems(props.orderItems); diff --git a/examples/core/src/app/(store)/account/orders/[orderId]/OrderLineItem.tsx b/examples/core/src/app/[lang]/(store)/account/orders/[orderId]/OrderLineItem.tsx similarity index 87% rename from examples/core/src/app/(store)/account/orders/[orderId]/OrderLineItem.tsx rename to examples/core/src/app/[lang]/(store)/account/orders/[orderId]/OrderLineItem.tsx index 9c690fbf..d0ddf63e 100644 --- a/examples/core/src/app/(store)/account/orders/[orderId]/OrderLineItem.tsx +++ b/examples/core/src/app/[lang]/(store)/account/orders/[orderId]/OrderLineItem.tsx @@ -1,5 +1,5 @@ import { ProductThumbnail } from "./ProductThumbnail"; -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; import { ElasticPathFile, OrderItemResponse } from "@epcc-sdk/sdks-shopper"; export function OrderLineItem({ @@ -12,20 +12,20 @@ export function OrderLineItem({ return (
    - + - +
    - +

    {orderItem.name}

    - +
    Quantity: {orderItem.quantity} diff --git a/examples/core/src/app/(store)/account/orders/[orderId]/ProductThumbnail.tsx b/examples/core/src/app/[lang]/(store)/account/orders/[orderId]/ProductThumbnail.tsx similarity index 100% rename from examples/core/src/app/(store)/account/orders/[orderId]/ProductThumbnail.tsx rename to examples/core/src/app/[lang]/(store)/account/orders/[orderId]/ProductThumbnail.tsx diff --git a/examples/core/src/app/(store)/account/orders/[orderId]/page.tsx b/examples/core/src/app/[lang]/(store)/account/orders/[orderId]/page.tsx similarity index 88% rename from examples/core/src/app/(store)/account/orders/[orderId]/page.tsx rename to examples/core/src/app/[lang]/(store)/account/orders/[orderId]/page.tsx index 45c702bf..d86aea40 100644 --- a/examples/core/src/app/(store)/account/orders/[orderId]/page.tsx +++ b/examples/core/src/app/[lang]/(store)/account/orders/[orderId]/page.tsx @@ -1,26 +1,27 @@ import { cookies } from "next/headers"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../../lib/cookie-constants"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { notFound, redirect } from "next/navigation"; -import { retrieveAccountMemberCredentials } from "../../../../../lib/retrieve-account-member-credentials"; -import { Button } from "../../../../../components/button/Button"; +import { retrieveAccountMemberCredentials } from "src/lib/retrieve-account-member-credentials"; +import { Button } from "src/components/button/Button"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; -import Link from "next/link"; -import { formatIsoDateString } from "../../../../../lib/format-iso-date-string"; +import { LocaleLink } from "src/components/LocaleLink"; +import { formatIsoDateString } from "src/lib/format-iso-date-string"; import { OrderLineItem } from "./OrderLineItem"; -import { createElasticPathClient } from "../../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { getAnOrder, getByContextAllProducts } from "@epcc-sdk/sdks-shopper"; import { resolveShopperOrder } from "../resolve-shopper-order"; -import { extractCartItemProductIds } from "../../../../../lib/extract-cart-item-product-ids"; -import { TAGS } from "../../../../../lib/constants"; +import { extractCartItemProductIds } from "src/lib/extract-cart-item-product-ids"; +import { TAGS } from "src/lib/constants"; import { extractCartItemMedia } from "../../../../(checkout)/checkout/extract-cart-item-media"; export const dynamic = "force-dynamic"; export default async function Order(props: { - params: Promise<{ orderId: string }>; + params: Promise<{ orderId: string, lang: string }>; }) { const params = await props.params; const cookieStore = await cookies(); + const lang = params?.lang; const accountMemberCookie = retrieveAccountMemberCredentials( cookieStore, @@ -28,7 +29,7 @@ export default async function Order(props: { ); if (!accountMemberCookie) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } const client = createElasticPathClient(); @@ -91,10 +92,10 @@ export default async function Order(props: {
    diff --git a/examples/core/src/app/(store)/account/orders/page.tsx b/examples/core/src/app/[lang]/(store)/account/orders/page.tsx similarity index 81% rename from examples/core/src/app/(store)/account/orders/page.tsx rename to examples/core/src/app/[lang]/(store)/account/orders/page.tsx index 9551631d..2cfc6c1e 100644 --- a/examples/core/src/app/(store)/account/orders/page.tsx +++ b/examples/core/src/app/[lang]/(store)/account/orders/page.tsx @@ -1,16 +1,16 @@ import { cookies } from "next/headers"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../lib/cookie-constants"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { notFound, redirect } from "next/navigation"; -import { retrieveAccountMemberCredentials } from "../../../../lib/retrieve-account-member-credentials"; -import { ResourcePagination } from "../../../../components/pagination/ResourcePagination"; -import { DEFAULT_PAGINATION_LIMIT, TAGS } from "../../../../lib/constants"; +import { retrieveAccountMemberCredentials } from "src/lib/retrieve-account-member-credentials"; +import { ResourcePagination } from "src/components/pagination/ResourcePagination"; +import { DEFAULT_PAGINATION_LIMIT, TAGS } from "src/lib/constants"; import { OrderItemWithDetails } from "./OrderItemWithDetails"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { getByContextAllProducts, getCustomerOrders, } from "@epcc-sdk/sdks-shopper"; -import { extractCartItemProductIds } from "../../../../lib/extract-cart-item-product-ids"; +import { extractCartItemProductIds } from "src/lib/extract-cart-item-product-ids"; import { extractCartItemMedia } from "../../../(checkout)/checkout/extract-cart-item-media"; import { resolveShopperOrder } from "./resolve-shopper-order"; @@ -22,10 +22,14 @@ export default async function Orders(props: { offset?: string; page?: string; }>; + params?: Promise<{ lang: string }>; }) { const searchParams = await props.searchParams; const limit = Number(searchParams?.limit) || DEFAULT_PAGINATION_LIMIT; const offset = Number(searchParams?.offset) || 0; + + const params = await props.params; + const lang = params?.lang; const cookieStore = await cookies(); @@ -35,7 +39,7 @@ export default async function Orders(props: { ); if (!accountMemberCookie) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } const client = await createElasticPathClient(); diff --git a/examples/core/src/app/(store)/account/orders/resolve-shopper-order.ts b/examples/core/src/app/[lang]/(store)/account/orders/resolve-shopper-order.ts similarity index 100% rename from examples/core/src/app/(store)/account/orders/resolve-shopper-order.ts rename to examples/core/src/app/[lang]/(store)/account/orders/resolve-shopper-order.ts diff --git a/examples/core/src/app/(store)/account/summary/YourInfoForm.tsx b/examples/core/src/app/[lang]/(store)/account/summary/YourInfoForm.tsx similarity index 88% rename from examples/core/src/app/(store)/account/summary/YourInfoForm.tsx rename to examples/core/src/app/[lang]/(store)/account/summary/YourInfoForm.tsx index 9fb96377..4edab55e 100644 --- a/examples/core/src/app/(store)/account/summary/YourInfoForm.tsx +++ b/examples/core/src/app/[lang]/(store)/account/summary/YourInfoForm.tsx @@ -1,9 +1,9 @@ "use client"; import { updateAccount } from "./actions"; -import { Label } from "../../../../components/label/Label"; -import { Input } from "../../../../components/input/Input"; -import { FormStatusButton } from "../../../../components/button/FormStatusButton"; +import { Label } from "src/components/label/Label"; +import { Input } from "src/components/input/Input"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; import { useState } from "react"; export function YourInfoForm({ diff --git a/examples/core/src/app/(store)/account/summary/actions.ts b/examples/core/src/app/[lang]/(store)/account/summary/actions.ts similarity index 81% rename from examples/core/src/app/(store)/account/summary/actions.ts rename to examples/core/src/app/[lang]/(store)/account/summary/actions.ts index 42da80d0..5668e18f 100644 --- a/examples/core/src/app/(store)/account/summary/actions.ts +++ b/examples/core/src/app/[lang]/(store)/account/summary/actions.ts @@ -2,11 +2,11 @@ import { z } from "zod"; import { cookies } from "next/headers"; -import { retrieveAccountMemberCredentials } from "../../../../lib/retrieve-account-member-credentials"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../lib/cookie-constants"; +import { retrieveAccountMemberCredentials } from "src/lib/retrieve-account-member-credentials"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { revalidatePath, revalidateTag } from "next/cache"; -import { getErrorMessage } from "../../../../lib/get-error-message"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; +import { getErrorMessage } from "src/lib/get-error-message"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { putV2AccountsAccountId, postV2AccountMembersTokens, diff --git a/examples/core/src/app/(store)/account/summary/page.tsx b/examples/core/src/app/[lang]/(store)/account/summary/page.tsx similarity index 81% rename from examples/core/src/app/(store)/account/summary/page.tsx rename to examples/core/src/app/[lang]/(store)/account/summary/page.tsx index c4ea6249..9cd77604 100644 --- a/examples/core/src/app/(store)/account/summary/page.tsx +++ b/examples/core/src/app/[lang]/(store)/account/summary/page.tsx @@ -1,24 +1,25 @@ import { cookies } from "next/headers"; -import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../../lib/cookie-constants"; +import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "src/lib/cookie-constants"; import { redirect } from "next/navigation"; import { getSelectedAccount, retrieveAccountMemberCredentials, -} from "../../../../lib/retrieve-account-member-credentials"; -import { Label } from "../../../../components/label/Label"; -import { Input } from "../../../../components/input/Input"; -import { FormStatusButton } from "../../../../components/button/FormStatusButton"; +} from "src/lib/retrieve-account-member-credentials"; +import { Label } from "src/components/label/Label"; +import { Input } from "src/components/input/Input"; +import { FormStatusButton } from "src/components/button/FormStatusButton"; import { YourInfoForm } from "./YourInfoForm"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { getV2AccountsAccountId, getV2AccountMembersAccountMemberId, } from "@epcc-sdk/sdks-shopper"; -import { TAGS } from "../../../../lib/constants"; +import { TAGS } from "src/lib/constants"; export const dynamic = "force-dynamic"; -export default async function AccountSummary() { +export default async function AccountSummary({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; const cookieStore = await cookies(); const accountMemberCookie = retrieveAccountMemberCredentials( @@ -27,7 +28,7 @@ export default async function AccountSummary() { ); if (!accountMemberCookie) { - return redirect("/login"); + return redirect(lang ? `/${lang}/login` : "/login"); } const client = createElasticPathClient(); diff --git a/examples/core/src/app/(store)/cart/CartItem.tsx b/examples/core/src/app/[lang]/(store)/cart/CartItem.tsx similarity index 94% rename from examples/core/src/app/(store)/cart/CartItem.tsx rename to examples/core/src/app/[lang]/(store)/cart/CartItem.tsx index 09be731d..49f0e4e7 100644 --- a/examples/core/src/app/(store)/cart/CartItem.tsx +++ b/examples/core/src/app/[lang]/(store)/cart/CartItem.tsx @@ -1,8 +1,8 @@ import { ProductThumbnail } from "../account/orders/[orderId]/ProductThumbnail"; -import { NumberInput } from "../../../components/number-input/NumberInput"; -import Link from "next/link"; -import { RemoveCartItemButton } from "../../../components/cart/RemoveCartItemButton"; -import { Item } from "../../../lib/group-cart-items"; +import { NumberInput } from "src/components/number-input/NumberInput"; +import { LocaleLink } from "src/components/LocaleLink"; +import { RemoveCartItemButton } from "src/components/cart/RemoveCartItemButton"; +import { Item } from "src/lib/group-cart-items"; import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import { calculateMultiItemOriginalTotal, calculateSaleAmount, calculateTotalSavings, getFormattedPercentage, getFormattedValue } from "src/lib/price-calculation"; @@ -63,9 +63,9 @@ export function CartItem({ item, thumbnail, currency }: CartItemProps) {
    {itemLink ? ( - + {item.name} - + ) : ( {item.name} )} @@ -182,7 +182,7 @@ export function CartItem({ item, thumbnail, currency }: CartItemProps) { <> )}
    - +
    diff --git a/examples/core/src/app/[lang]/(store)/cart/CartItemWide.tsx b/examples/core/src/app/[lang]/(store)/cart/CartItemWide.tsx new file mode 100644 index 00000000..65a520c3 --- /dev/null +++ b/examples/core/src/app/[lang]/(store)/cart/CartItemWide.tsx @@ -0,0 +1,186 @@ +import { ProductThumbnail } from "../account/orders/[orderId]/ProductThumbnail"; +import { LocaleLink } from "src/components/LocaleLink"; +import { NumberInput } from "src/components/number-input/NumberInput"; +import { CartItemProps } from "./CartItem"; +import { RemoveCartItemButton } from "src/components/cart/RemoveCartItemButton"; +import { calculateMultiItemOriginalTotal, calculateSaleAmount, calculateTotalSavings, getFormattedPercentage, getFormattedValue } from "src/lib/price-calculation"; + +export function CartItemWide({ item, thumbnail, currency }: CartItemProps) { + if (!item) { + return
    Missing cart item data
    ; + } + + let itemLink = null; + if (item.product_id) { + itemLink = `/products/${item.product_id}`; + } + + const itemDisplayPrice = item.meta?.display_price?.with_tax?.unit; + const fallbackDisplayPrice = (item as any).productDetail?.meta?.display_price?.without_tax; + const originalDisplayPrice = (item as any).productDetail?.meta?.original_display_price?.without_tax; + const finalOriginalPrice = originalDisplayPrice + ? originalDisplayPrice + : fallbackDisplayPrice && + fallbackDisplayPrice.amount !== itemDisplayPrice?.amount + ? fallbackDisplayPrice + : undefined; + + // TOTAL BEFORE SALE PRICING + const multiItemOriginalTotal = calculateMultiItemOriginalTotal(item); + const formattedMultiItemOriginalTotal = getFormattedValue(multiItemOriginalTotal!, currency!); + + // SALE SAVINGS CALCULATION + const saleAmount = calculateSaleAmount(item); + const formattedSaleAmount = getFormattedValue(saleAmount!, currency!); + + // SALE SAVINGS PERCENTAGE CALCULATION + const formattedSalePercentage = getFormattedPercentage(saleAmount!, multiItemOriginalTotal!); + + // TOTAL SAVINGS CALCULATION + const itemTotalSavings = calculateTotalSavings(item); + const formattedTotalSavings = getFormattedValue(itemTotalSavings!, currency!); + + // TOTAL SAVINGS PERCENTAGE CALCULATION + const itemWithoutDiscountAmount = item.meta?.display_price?.without_discount?.value?.amount; + const discountPercentFormatted = getFormattedPercentage(itemTotalSavings!, (multiItemOriginalTotal || itemWithoutDiscountAmount)!); + + // ITEM PROMOTIONS + const itemDiscounts = (item as any)?.meta?.display_price?.discounts as Record | undefined; + + return ( +
    + {/* Thumbnail */} +
    + +
    + {/* Details */} +
    +
    +
    + {itemLink ? ( + + {item.name} + + ) : ( + {item.name} + )} + + + Quantity: {item.quantity} + +
    + +
    + + {item.meta?.display_price?.with_tax?.unit?.formatted} + + + {!finalOriginalPrice && + item.meta?.display_price?.without_discount?.unit?.amount && + item.meta?.display_price.without_discount.unit?.amount !== + item.meta?.display_price.with_tax?.unit?.amount && ( + + {item.meta?.display_price.without_discount.unit?.formatted} + + )} + + {finalOriginalPrice?.amount && + item.meta?.display_price && + finalOriginalPrice?.amount !== + item.meta?.display_price.with_tax?.unit?.amount && ( + + {finalOriginalPrice?.formatted} + + )} + + {originalDisplayPrice ? ( + + SALE + + ) : ( + <> + )} +
    +
    + + {item.quantity > 1 && ( +
    +
    + + Line total: ({item.quantity} x{" "} + {item.meta?.display_price?.with_tax?.unit?.formatted}) + +
    + +
    + + {item.meta?.display_price?.with_tax?.value?.formatted} + + + {!formattedMultiItemOriginalTotal && + item.meta?.display_price?.without_discount?.value?.amount && + item.meta?.display_price.without_discount.value?.amount !== + item.meta?.display_price.with_tax?.value?.amount && ( + + {item.meta?.display_price.without_discount.value?.formatted} + + )} + + {formattedMultiItemOriginalTotal && ( + + {formattedMultiItemOriginalTotal} + + )} +
    +
    + )} + + {/* SALE PRICE */} + {formattedSaleAmount ? ( +
    + + {originalDisplayPrice + ? `Sale (${formattedSalePercentage} off)` + : `Bulk offer (${formattedSalePercentage} off)`} + + + ({formattedSaleAmount}) + +
    + ) : ( + <> + )} + + {/* PROMO PRICES */} + {itemDiscounts && + Object.entries(itemDiscounts).map(([key, discount]) => ( +
    + Promo ({key}) + + ({discount.formatted}) + +
    + ))} + + {/* TOTAL SAVINGS */} + {item.meta?.display_price?.discount?.value?.amount ? ( +
    + + You save{" "} + + {formattedTotalSavings} + {" "} + ({discountPercentFormatted}) + +
    + ) : ( + <> + )} +
    + + +
    +
    +
    + ); +} diff --git a/examples/core/src/app/[lang]/(store)/cart/CartSidebar.tsx b/examples/core/src/app/[lang]/(store)/cart/CartSidebar.tsx new file mode 100644 index 00000000..94ab9b77 --- /dev/null +++ b/examples/core/src/app/[lang]/(store)/cart/CartSidebar.tsx @@ -0,0 +1,123 @@ +import { Separator } from "src/components/separator/Separator"; +import { CartDiscounts } from "src/components/cart/CartDiscounts"; +import * as React from "react"; +import { + ItemSidebarPromotions, + ItemSidebarSumTotal, + ItemSidebarTotals, + ItemSidebarTotalsTax, +} from "src/components/checkout-sidebar/ItemSidebar"; +import { getACart, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; +import { groupCartItems } from "src/lib/group-cart-items"; +import { formatCurrency } from "src/lib/format-currency"; + +export function CartSidebar({ + cart, + storeCurrency, +}: { + cart: NonNullable>["data"]>; + storeCurrency: ResponseCurrency | undefined; +}) { + const meta = cart.data?.meta!; + const groupedItems = groupCartItems(cart.included?.items ?? []); + + const discountedValues = cart.data?.meta?.display_price?.discount; + const hasPromotion = discountedValues && discountedValues.amount !== 0; + + // CART TOTAL BEFORE DISCOUNTS (SALE + PROMOTIONS) + const cartItems = cart.included?.items ?? []; + const totalCartValue = cartItems.reduce((acc, item) => { + const itemOriginalPrice = + (item as any).productDetail?.meta?.original_display_price?.without_tax + ?.amount || + (item as any).productDetail?.meta?.display_price?.without_tax?.amount || + 0 + const itemQuantity = (item as any).quantity || 1; + return acc + (itemOriginalPrice * itemQuantity) + }, 0); + const formattedCartTotal = totalCartValue + ? formatCurrency( + totalCartValue || 0, + storeCurrency || { code: "USD", decimal_places: 2 }, + ) + : undefined + + // TOTAL SALE PRICE SAVINGS + const totalCartValueWithoutDiscount = cartItems.reduce((acc, item) => { + const itemWithoudDiscount = + item?.meta?.display_price?.without_discount?.value?.amount || 0 + return acc + itemWithoudDiscount + }, 0); + const cartSavings = totalCartValueWithoutDiscount - totalCartValue + const formattedCartSavings = cartSavings + ? formatCurrency( + cartSavings || 0, + storeCurrency || { code: "USD", decimal_places: 2 }, + ) + : undefined + const hasSalePricing = cartSavings !== 0 + + // TOTAL CART SAVINGS + const totalCartValueWithoutTax = cart.data?.meta?.display_price?.without_tax?.amount || 0; + const totalCartSavings = totalCartValueWithoutTax - totalCartValue + const formattedTotalCartSavings = totalCartSavings + ? formatCurrency( + totalCartSavings || 0, + storeCurrency || { code: "USD", decimal_places: 2 }, + ) + : undefined + + return ( +
    + + + + {/* Totals */} + + {(hasSalePricing || hasPromotion) && ( + <> +
    + Total before discounts + {formattedCartTotal} +
    + {hasSalePricing && ( +
    + Sale markdowns + {formattedCartSavings} +
    + )} + {hasPromotion && ( +
    + Promo savings + + {discountedValues.formatted} + +
    + )} + + )} +
    + Sub Total + + {cart.data?.meta?.display_price?.without_tax?.formatted} + +
    +
    + Shipping + Calculated at checkout +
    + +
    + + {/* Sum Total */} + + {/* Total Savings */} + {(hasSalePricing || hasPromotion) && ( +
    + Your savings + {formattedTotalCartSavings} +
    + )} +
    + ); +} diff --git a/examples/core/src/app/(store)/cart/YourBag.tsx b/examples/core/src/app/[lang]/(store)/cart/YourBag.tsx similarity index 77% rename from examples/core/src/app/(store)/cart/YourBag.tsx rename to examples/core/src/app/[lang]/(store)/cart/YourBag.tsx index d2fa5af6..24932e9b 100644 --- a/examples/core/src/app/(store)/cart/YourBag.tsx +++ b/examples/core/src/app/[lang]/(store)/cart/YourBag.tsx @@ -1,11 +1,13 @@ import { CartItemWide } from "./CartItemWide"; -import { CartIncluded } from "@epcc-sdk/sdks-shopper"; -import { groupCartItems } from "../../../lib/group-cart-items"; +import { CartIncluded, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; +import { groupCartItems } from "src/lib/group-cart-items"; export function YourBag({ cart, + currency, }: { cart: NonNullable; + currency?: ResponseCurrency; }) { const groupedItems = groupCartItems(cart ?? []); const items = [ @@ -21,7 +23,7 @@ export function YourBag({ key={item.id} className="self-stretch border-t border-zinc-300 py-5" > - + ); })} diff --git a/examples/core/src/app/[lang]/(store)/cart/page.tsx b/examples/core/src/app/[lang]/(store)/cart/page.tsx new file mode 100644 index 00000000..0537803b --- /dev/null +++ b/examples/core/src/app/[lang]/(store)/cart/page.tsx @@ -0,0 +1,123 @@ +import { YourBag } from "./YourBag"; +import { CartSidebar } from "./CartSidebar"; +import { Button } from "src/components/button/Button"; +import { LocaleLink } from "src/components/LocaleLink"; +import { LockClosedIcon } from "@heroicons/react/24/solid"; +import { getACart, getAllCurrencies, getByContextProduct } from "@epcc-sdk/sdks-shopper"; +import { CART_COOKIE_NAME } from "src/lib/cookie-constants"; +import { cookies } from "next/headers"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; +import { TAGS } from "src/lib/constants"; +import { getPreferredCurrency } from "src/lib/i18n"; + +export default async function CartPage({ params }: { params: Promise<{ lang: string }> }) { + const cartCookie = (await cookies()).get(CART_COOKIE_NAME); + const client = createElasticPathClient(); + const { lang } = await params; + + if (!cartCookie) { + throw new Error("Cart cookie not found"); + } + + const currencies = await getAllCurrencies({ + client, + next: { + tags: [TAGS.currencies], + }, + }); + const currency = getPreferredCurrency(lang, currencies.data?.data || []); + + const cartResponse = await getACart({ + path: { + cartID: cartCookie.value, + }, + query: { + include: ["items", "promotions", "tax_items", "custom_discounts"], + }, + client, + next: { + tags: [TAGS.cart], + }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currency?.code, + } + }); + + const cartCurrency = cartResponse.data?.data?.meta?.display_price?.with_tax?.currency; + const currencyUpdated = getPreferredCurrency(lang, currencies.data?.data || [], cartCurrency); + + // Fetch product details for each cart item to get original sale price + const cartItems = cartResponse?.data?.included?.items; + const productDetailsPromises = cartItems?.map(item => + getByContextProduct({ + client, + path: { product_id: item.product_id! }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyUpdated?.code, + } + }) + ); + const productDetails = await Promise.all(productDetailsPromises || []); + + // Merge product details into cart items + const cartItemsWithDetails = cartResponse?.data?.included?.items?.map(item => { + const productDetail = productDetails.find(pd => pd.data?.data?.id === item.product_id)?.data?.data; + return { + ...item, + productDetail, + }; + }); + + if (!cartResponse.data) { + return
    Cart items not found
    ; + } + + const items = cartItemsWithDetails!; + + return ( + <> + {items?.length && items.length > 0 ? ( +
    + {/* Main Content */} +
    +
    +

    Your Bag

    + {/* Cart Items */} + +
    +
    + {/* Sidebar */} +
    + + +
    +
    + ) : ( + <> + {/* Empty Cart */} +
    +

    + Empty Cart +

    +

    Your cart is empty

    +
    + +
    +
    + + )} + + ); +} diff --git a/examples/core/src/app/(store)/faq/page.tsx b/examples/core/src/app/[lang]/(store)/faq/page.tsx similarity index 55% rename from examples/core/src/app/(store)/faq/page.tsx rename to examples/core/src/app/[lang]/(store)/faq/page.tsx index 786734bc..4151724f 100644 --- a/examples/core/src/app/(store)/faq/page.tsx +++ b/examples/core/src/app/[lang]/(store)/faq/page.tsx @@ -1,4 +1,4 @@ -import Blurb from "../../../components/shared/blurb"; +import Blurb from "src/components/shared/blurb"; export default function FAQ() { return ; diff --git a/examples/core/src/app/(store)/layout.tsx b/examples/core/src/app/[lang]/(store)/layout.tsx similarity index 69% rename from examples/core/src/app/(store)/layout.tsx rename to examples/core/src/app/[lang]/(store)/layout.tsx index 97f1d40e..692c4383 100644 --- a/examples/core/src/app/(store)/layout.tsx +++ b/examples/core/src/app/[lang]/(store)/layout.tsx @@ -1,11 +1,11 @@ import { ReactNode, Suspense } from "react"; import localFont from "next/font/local"; -import { getStoreInitialState } from "../../lib/get-store-initial-state"; +import { getStoreInitialState } from "src/lib/get-store-initial-state"; import { Providers } from "../providers"; -import Header from "../../components/header/Header"; -import { Toaster } from "../../components/toast/toaster"; -import Footer from "../../components/footer/Footer"; -import { createElasticPathClient } from "../../lib/create-elastic-path-client"; +import Header from "src/components/header/Header"; +import { Toaster } from "src/components/toast/toaster"; +import Footer from "src/components/footer/Footer"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; const { SITE_NAME } = process.env; const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL @@ -31,26 +31,31 @@ export const metadata = { export const revalidate = 300; const inter = localFont({ - src: "../../../public/fonts/Inter-VariableFont_opsz,wght.ttf", + src: "../../../../public/fonts/Inter-VariableFont_opsz,wght.ttf", display: "swap", variable: "--font-inter", }); export default async function StoreLayout({ children, + params, }: { children: ReactNode; + params: Promise<{ lang: string }>; }) { + const resolvedParams = await params; + const lang = resolvedParams?.lang; + const client = await createElasticPathClient(); const initialState = await getStoreInitialState(client); return ( - + {/* headless ui needs this div - https://github.com/tailwindlabs/headlessui/issues/2752#issuecomment-1745272229 */}
    -
    +
    {children}
    diff --git a/examples/core/src/app/(store)/not-found.tsx b/examples/core/src/app/[lang]/(store)/not-found.tsx similarity index 64% rename from examples/core/src/app/(store)/not-found.tsx rename to examples/core/src/app/[lang]/(store)/not-found.tsx index d97d88f4..b568b0d7 100644 --- a/examples/core/src/app/(store)/not-found.tsx +++ b/examples/core/src/app/[lang]/(store)/not-found.tsx @@ -1,13 +1,13 @@ -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; export default function NotFound() { return (
    404 - The page could not be found. - + Back to home - +
    ); } diff --git a/examples/core/src/app/(store)/page.tsx b/examples/core/src/app/[lang]/(store)/page.tsx similarity index 84% rename from examples/core/src/app/(store)/page.tsx rename to examples/core/src/app/[lang]/(store)/page.tsx index 6f87cd28..074b4c9f 100644 --- a/examples/core/src/app/(store)/page.tsx +++ b/examples/core/src/app/[lang]/(store)/page.tsx @@ -1,5 +1,5 @@ -import PromotionBanner from "../../components/promotion-banner/PromotionBanner"; -import FeaturedProducts from "../../components/featured-products/FeaturedProducts"; +import PromotionBanner from "src/components/promotion-banner/PromotionBanner"; +import FeaturedProducts from "src/components/featured-products/FeaturedProducts"; import { Suspense } from "react"; export default async function Home() { diff --git a/examples/core/src/app/(store)/products/[productId]/actions/cart-actions.ts b/examples/core/src/app/[lang]/(store)/products/[productId]/actions/cart-actions.ts similarity index 75% rename from examples/core/src/app/(store)/products/[productId]/actions/cart-actions.ts rename to examples/core/src/app/[lang]/(store)/products/[productId]/actions/cart-actions.ts index ad0ac9b8..534e66d2 100644 --- a/examples/core/src/app/(store)/products/[productId]/actions/cart-actions.ts +++ b/examples/core/src/app/[lang]/(store)/products/[productId]/actions/cart-actions.ts @@ -2,24 +2,26 @@ import { cookies } from "next/headers"; import { z } from "zod"; -import { CART_COOKIE_NAME } from "../../../../../lib/cookie-constants"; -import { simpleProductSchema } from "../../../../../components/product/standard/SimpleProductForm"; +import { CART_COOKIE_NAME } from "src/lib/cookie-constants"; +import { simpleProductSchema } from "src/components/product/standard/SimpleProductForm"; import { manageCarts, deleteACartItem, updateACartItem, deleteAPromotionViaPromotionCode, + deleteAllCartItems, } from "@epcc-sdk/sdks-shopper"; import { revalidateTag } from "next/cache"; -import { createElasticPathClient } from "../../../../../lib/create-elastic-path-client"; -import { createBundleFormSchema } from "../../../../../components/product/bundles/validation-schema"; -import { formSelectedOptionsToData } from "../../../../../components/product/bundles/form-parsers"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; +import { createBundleFormSchema } from "src/components/product/bundles/validation-schema"; +import { formSelectedOptionsToData } from "src/components/product/bundles/form-parsers"; /** * addToCartAction - Server Action that adds an item to the cart */ export async function addToCartAction( data: z.infer, + currencyCode?: string ) { const cartCookie = (await cookies()).get(CART_COOKIE_NAME); @@ -40,6 +42,9 @@ export async function addToCartAction( ...(data.location && { location: data.location }), }, }, + headers: { + "X-Moltin-Currency": currencyCode, + } }); await revalidateTag("cart"); @@ -55,6 +60,7 @@ export async function addToCartAction( */ export async function addToBundleAction( data: z.infer>, + currencyCode?: string ) { const cartCookie = (await cookies()).get(CART_COOKIE_NAME); @@ -78,6 +84,9 @@ export async function addToBundleAction( ...(data.location && { location: data.location }), }, }, + headers: { + "X-Moltin-Currency": currencyCode, + } }); await revalidateTag("cart"); @@ -120,7 +129,7 @@ export async function updateCartItemAction(data: { cartItemId: string; quantity: number; location?: string; -}) { +}, currencyCode?: string ) { const cartCookie = (await cookies()).get(CART_COOKIE_NAME); if (!cartCookie) { @@ -139,6 +148,9 @@ export async function updateCartItemAction(data: { ...(data.location && { location: data.location }), }, }, + headers: { + "X-Moltin-Currency": currencyCode, + }, }); await revalidateTag("cart"); @@ -172,3 +184,28 @@ export async function removeCartPromotionAction(data: { promoCode: string }) { error: result.error, }; } + +/** + * removeAllCartItemsAction - Server Action that removes all items from the cart + */ +export async function removeAllCartItemsAction() { + const cartCookie = (await cookies()).get(CART_COOKIE_NAME); + + if (!cartCookie) { + throw new Error("No cart cookie found. Cannot remove product from cart."); + } + + const client = createElasticPathClient(); + + const result = await deleteAllCartItems({ + client, + path: { cartID: cartCookie.value }, + }); + + await revalidateTag("cart"); + + return { + data: result.data, + error: result.error, + }; +} diff --git a/examples/core/src/app/(store)/products/[productId]/page.tsx b/examples/core/src/app/[lang]/(store)/products/[productId]/page.tsx similarity index 76% rename from examples/core/src/app/(store)/products/[productId]/page.tsx rename to examples/core/src/app/[lang]/(store)/products/[productId]/page.tsx index 0c7b4cd7..52742f03 100644 --- a/examples/core/src/app/(store)/products/[productId]/page.tsx +++ b/examples/core/src/app/[lang]/(store)/products/[productId]/page.tsx @@ -7,19 +7,22 @@ import { getAllFiles, ElasticPathFile, listLocations, + getAllCurrencies, } from "@epcc-sdk/sdks-shopper"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; -import { SimpleProductContent } from "../../../../components/product/standard/SimpleProductContent"; -import { SimpleProductProvider } from "../../../../components/product/standard/SimpleProductProvider"; -import { VariationProductProvider } from "../../../../components/product/variations/VariationProductProvider"; -import { VariationProductContent } from "../../../../components/product/variations/VariationProductContent"; -import { BundleProductProvider } from "../../../../components/product/bundles/BundleProductProvider"; -import { BundleProductContent } from "../../../../components/product/bundles/BundleProductContent"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; +import { SimpleProductContent } from "src/components/product/standard/SimpleProductContent"; +import { SimpleProductProvider } from "src/components/product/standard/SimpleProductProvider"; +import { VariationProductProvider } from "src/components/product/variations/VariationProductProvider"; +import { VariationProductContent } from "src/components/product/variations/VariationProductContent"; +import { BundleProductProvider } from "src/components/product/bundles/BundleProductProvider"; +import { BundleProductContent } from "src/components/product/bundles/BundleProductContent"; +import { getPreferredCurrency } from "src/lib/i18n"; +import { TAGS } from "src/lib/constants"; export const dynamic = "force-dynamic"; type Props = { - params: Promise<{ productId: string }>; + params: Promise<{ productId: string; lang?: string; }>; }; export async function generateMetadata(props: Props): Promise { @@ -35,7 +38,7 @@ export async function generateMetadata(props: Props): Promise { }, query: { include: ["main_image", "files", "component_products"], - }, + } }); if (!product.data?.data) { @@ -51,6 +54,13 @@ export async function generateMetadata(props: Props): Promise { export default async function ProductPage(props: Props) { const params = await props.params; const client = createElasticPathClient(); + const currencies = await getAllCurrencies({ + client, + next: { + tags: [TAGS.currencies], + }, + }); + const currency = getPreferredCurrency(params?.lang, currencies.data?.data || []); const productPromise = getByContextProduct({ client, path: { @@ -59,7 +69,12 @@ export default async function ProductPage(props: Props) { query: { include: ["main_image", "files", "component_products"], }, + headers: { + "Accept-Language": params.lang, + "X-Moltin-Currency": currency?.code + } }); + const inventoryPromise = getStock({ client, path: { @@ -111,6 +126,7 @@ export default async function ProductPage(props: Props) { product={productResponse.data} inventory={inventoryResponse.data?.data} locations={locationResponse.data?.data} + currency={currency} > @@ -123,6 +139,7 @@ export default async function ProductPage(props: Props) { componentImageFiles={componentImageFiles} inventory={inventoryResponse.data?.data} locations={locationResponse.data?.data} + currency={currency} > @@ -136,6 +153,7 @@ export default async function ProductPage(props: Props) { parentProduct={parentProduct?.data} inventory={inventoryResponse.data?.data} locations={locationResponse.data?.data} + currency={currency} > @@ -147,6 +165,7 @@ export default async function ProductPage(props: Props) { product={productResponse.data} inventory={inventoryResponse.data?.data} locations={locationResponse.data?.data} + currency={currency} > diff --git a/examples/core/src/app/(store)/search/[[...node]]/layout.tsx b/examples/core/src/app/[lang]/(store)/search/[[...node]]/layout.tsx similarity index 79% rename from examples/core/src/app/(store)/search/[[...node]]/layout.tsx rename to examples/core/src/app/[lang]/(store)/search/[[...node]]/layout.tsx index 937b6e50..d213c077 100644 --- a/examples/core/src/app/(store)/search/[[...node]]/layout.tsx +++ b/examples/core/src/app/[lang]/(store)/search/[[...node]]/layout.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import Breadcrumb from "../../../../components/breadcrumb"; +import Breadcrumb from "src/components/breadcrumb"; export default function SearchLayout({ children }: { children: ReactNode }) { return ( diff --git a/examples/core/src/app/(store)/search/[[...node]]/page.tsx b/examples/core/src/app/[lang]/(store)/search/[[...node]]/page.tsx similarity index 98% rename from examples/core/src/app/(store)/search/[[...node]]/page.tsx rename to examples/core/src/app/[lang]/(store)/search/[[...node]]/page.tsx index b4d6f5aa..676afcc8 100644 --- a/examples/core/src/app/(store)/search/[[...node]]/page.tsx +++ b/examples/core/src/app/[lang]/(store)/search/[[...node]]/page.tsx @@ -1,7 +1,7 @@ import { Search } from "../search"; import { Metadata } from "next"; import { notFound } from "next/navigation"; -import { createElasticPathClient } from "../../../../lib/create-elastic-path-client"; +import { createElasticPathClient } from "src/lib/create-elastic-path-client"; import { Client, getByContextAllHierarchies, diff --git a/examples/core/src/app/(store)/search/search.tsx b/examples/core/src/app/[lang]/(store)/search/search.tsx similarity index 83% rename from examples/core/src/app/(store)/search/search.tsx rename to examples/core/src/app/[lang]/(store)/search/search.tsx index f88f2ab8..9254dea8 100644 --- a/examples/core/src/app/(store)/search/search.tsx +++ b/examples/core/src/app/[lang]/(store)/search/search.tsx @@ -1,5 +1,5 @@ "use client"; -import SearchResults from "../../../components/search/SearchResults"; +import SearchResults from "src/components/search/SearchResults"; import React from "react"; import { usePathname } from "next/navigation"; import { ProductListData } from "@epcc-sdk/sdks-shopper"; diff --git a/examples/core/src/app/(store)/shipping/page.tsx b/examples/core/src/app/[lang]/(store)/shipping/page.tsx similarity index 58% rename from examples/core/src/app/(store)/shipping/page.tsx rename to examples/core/src/app/[lang]/(store)/shipping/page.tsx index d5ee20b4..43cbfb5f 100644 --- a/examples/core/src/app/(store)/shipping/page.tsx +++ b/examples/core/src/app/[lang]/(store)/shipping/page.tsx @@ -1,4 +1,4 @@ -import Blurb from "../../../components/shared/blurb"; +import Blurb from "src/components/shared/blurb"; export default function Shipping() { return ; diff --git a/examples/core/src/app/(store)/support/page.tsx b/examples/core/src/app/[lang]/(store)/support/page.tsx similarity index 58% rename from examples/core/src/app/(store)/support/page.tsx rename to examples/core/src/app/[lang]/(store)/support/page.tsx index 13430886..3db9642e 100644 --- a/examples/core/src/app/(store)/support/page.tsx +++ b/examples/core/src/app/[lang]/(store)/support/page.tsx @@ -1,4 +1,4 @@ -import Blurb from "../../../components/shared/blurb"; +import Blurb from "src/components/shared/blurb"; export default function Support() { return ; diff --git a/examples/core/src/app/(store)/terms/page.tsx b/examples/core/src/app/[lang]/(store)/terms/page.tsx similarity index 60% rename from examples/core/src/app/(store)/terms/page.tsx rename to examples/core/src/app/[lang]/(store)/terms/page.tsx index 3b651abc..655a926a 100644 --- a/examples/core/src/app/(store)/terms/page.tsx +++ b/examples/core/src/app/[lang]/(store)/terms/page.tsx @@ -1,4 +1,4 @@ -import Blurb from "../../../components/shared/blurb"; +import Blurb from "src/components/shared/blurb"; export default function Terms() { return ; diff --git a/examples/core/src/app/configuration-error/page.tsx b/examples/core/src/app/[lang]/configuration-error/page.tsx similarity index 95% rename from examples/core/src/app/configuration-error/page.tsx rename to examples/core/src/app/[lang]/configuration-error/page.tsx index 6ce5e299..b67b3869 100644 --- a/examples/core/src/app/configuration-error/page.tsx +++ b/examples/core/src/app/[lang]/configuration-error/page.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; import { Metadata } from "next"; export const metadata: Metadata = { @@ -29,12 +29,12 @@ export default async function ConfigurationErrorPage(props: Props) { There is a problem with the stores setup - Refresh - + diff --git a/examples/core/src/app/error.tsx b/examples/core/src/app/[lang]/error.tsx similarity index 79% rename from examples/core/src/app/error.tsx rename to examples/core/src/app/[lang]/error.tsx index f4724026..d96ffebd 100644 --- a/examples/core/src/app/error.tsx +++ b/examples/core/src/app/[lang]/error.tsx @@ -1,5 +1,5 @@ "use client"; -import Link from "next/link"; +import { LocaleLink } from "../../components/LocaleLink"; export default function GlobalError({ error, @@ -15,9 +15,9 @@ export default function GlobalError({ {error.digest} - Internal server error. - + Back to home - + @@ -242,7 +243,7 @@ export function CartSheet({ asChild className="self-stretch" > - Go to bag + Go to bag diff --git a/examples/core/src/components/cart/RemoveCartItemButton.tsx b/examples/core/src/components/cart/RemoveCartItemButton.tsx index 465ee396..18dd190d 100644 --- a/examples/core/src/components/cart/RemoveCartItemButton.tsx +++ b/examples/core/src/components/cart/RemoveCartItemButton.tsx @@ -2,7 +2,7 @@ import { useNotify } from "../../hooks/use-event"; import { useQueryClient } from "@tanstack/react-query"; import { useTransition } from "react"; -import { removeCartItemAction } from "../../app/(store)/products/[productId]/actions/cart-actions"; +import { removeCartItemAction } from "../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../lib/cookie-constants"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; diff --git a/examples/core/src/components/cart/RemoveCartItemXButton.tsx b/examples/core/src/components/cart/RemoveCartItemXButton.tsx index 9b43b13c..82a5bad3 100644 --- a/examples/core/src/components/cart/RemoveCartItemXButton.tsx +++ b/examples/core/src/components/cart/RemoveCartItemXButton.tsx @@ -2,7 +2,7 @@ import { useNotify } from "../../hooks/use-event"; import { useQueryClient } from "@tanstack/react-query"; import { useTransition } from "react"; -import { removeCartItemAction } from "../../app/(store)/products/[productId]/actions/cart-actions"; +import { removeCartItemAction } from "../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../lib/cookie-constants"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; diff --git a/examples/core/src/components/cart/RemoveCartPromotionXButton.tsx b/examples/core/src/components/cart/RemoveCartPromotionXButton.tsx index c2363441..1247305a 100644 --- a/examples/core/src/components/cart/RemoveCartPromotionXButton.tsx +++ b/examples/core/src/components/cart/RemoveCartPromotionXButton.tsx @@ -2,7 +2,7 @@ import { useNotify } from "../../hooks/use-event"; import { useQueryClient } from "@tanstack/react-query"; import { useTransition } from "react"; -import { removeCartPromotionAction } from "../../app/(store)/products/[productId]/actions/cart-actions"; +import { removeCartPromotionAction } from "../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../lib/cookie-constants"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; diff --git a/examples/core/src/components/checkout-item/CheckoutItem.tsx b/examples/core/src/components/checkout-item/CheckoutItem.tsx index c9e92341..cdabd7a3 100644 --- a/examples/core/src/components/checkout-item/CheckoutItem.tsx +++ b/examples/core/src/components/checkout-item/CheckoutItem.tsx @@ -1,8 +1,7 @@ "use client"; -import { ProductThumbnail } from "../../app/(store)/account/orders/[orderId]/ProductThumbnail"; -import Link from "next/link"; +import { ProductThumbnail } from "../../app/[lang]/(store)/account/orders/[orderId]/ProductThumbnail"; +import { LocaleLink } from "../LocaleLink"; import { Item } from "../../lib/group-cart-items"; -import { formatCurrency } from "src/lib/format-currency"; import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import { calculateMultiItemOriginalTotal, calculateSaleAmount, calculateTotalSavings, getFormattedPercentage, getFormattedValue } from "src/lib/price-calculation"; @@ -62,9 +61,9 @@ export function CheckoutItem({
    - + {item.name} - + Quantity: {item.quantity}
    diff --git a/examples/core/src/components/checkout-sidebar/AddPromotion.tsx b/examples/core/src/components/checkout-sidebar/AddPromotion.tsx index a45c1900..ccc67e56 100644 --- a/examples/core/src/components/checkout-sidebar/AddPromotion.tsx +++ b/examples/core/src/components/checkout-sidebar/AddPromotion.tsx @@ -13,6 +13,8 @@ import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../lib/cookie-constants"; import { useNotify } from "../../hooks/use-event"; +import { useParams } from "next/navigation"; +import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; const cartErrorOptions = { scope: "cart", @@ -20,7 +22,7 @@ const cartErrorOptions = { action: "add-promotion", } as const; -export function AddPromotion() { +export function AddPromotion(props: {currencyCode: string | undefined}) { const [showInput, setShowInput] = useState(false); const queryClient = useQueryClient(); const [error, setError] = useState(undefined); @@ -30,7 +32,7 @@ export function AddPromotion() { setError(undefined); try { - const result = await applyDiscount(formData); + const result = await applyDiscount(formData, props?.currencyCode || ""); if (result.error) { notify({ diff --git a/examples/core/src/components/checkout-sidebar/ItemSidebar.tsx b/examples/core/src/components/checkout-sidebar/ItemSidebar.tsx index 8930e233..57259b80 100644 --- a/examples/core/src/components/checkout-sidebar/ItemSidebar.tsx +++ b/examples/core/src/components/checkout-sidebar/ItemSidebar.tsx @@ -91,10 +91,10 @@ export function ItemSidebarItems({ items, storeCurrency }: { items: Item[], stor ); } -export function ItemSidebarPromotions() { +export function ItemSidebarPromotions(props: {currencyCode: string | undefined}) { return (
    - +
    ); } diff --git a/examples/core/src/components/checkout-sidebar/actions.ts b/examples/core/src/components/checkout-sidebar/actions.ts index 30d0d5e9..0d553755 100644 --- a/examples/core/src/components/checkout-sidebar/actions.ts +++ b/examples/core/src/components/checkout-sidebar/actions.ts @@ -11,7 +11,7 @@ const applyDiscountSchema = z.object({ code: z.string(), }); -export async function applyDiscount(formData: FormData) { +export async function applyDiscount(formData: FormData, currencyCode: string) { try { const client = await createElasticPathClient(); @@ -43,6 +43,9 @@ export async function applyDiscount(formData: FormData) { code: validatedFormData.data.code, }, }, + headers: { + "X-Moltin-Currency": currencyCode, + }, }); await revalidateTag("cart"); diff --git a/examples/core/src/components/featured-products/FeaturedProducts.tsx b/examples/core/src/components/featured-products/FeaturedProducts.tsx index fa5c52d1..bf786a5c 100644 --- a/examples/core/src/components/featured-products/FeaturedProducts.tsx +++ b/examples/core/src/components/featured-products/FeaturedProducts.tsx @@ -1,6 +1,6 @@ "use server"; import clsx from "clsx"; -import Link from "next/link"; +import { LocaleLink } from "../LocaleLink"; import { ArrowRightIcon, EyeSlashIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import { fetchFeaturedProducts } from "./fetchFeaturedProducts"; @@ -33,14 +33,14 @@ export default async function FeaturedProducts({ {title} {linkProps && ( - {linkProps.text} - + )}
      {products.map((product) => ( - +
    • @@ -78,7 +78,7 @@ export default async function FeaturedProducts({ {product.meta?.display_price?.without_tax?.formatted}

    • - +
      ))}
    diff --git a/examples/core/src/components/footer/Footer.tsx b/examples/core/src/components/footer/Footer.tsx index 2bfe4faa..4b9c2fa0 100644 --- a/examples/core/src/components/footer/Footer.tsx +++ b/examples/core/src/components/footer/Footer.tsx @@ -1,77 +1,81 @@ +"use client"; import Link from "next/link"; +import { LocaleLink } from "../LocaleLink"; import { PhoneIcon, InformationCircleIcon } from "@heroicons/react/24/solid"; import { GitHubIcon } from "../icons/github-icon"; import EpLogo from "../icons/ep-logo"; import type { JSX } from "react"; -const Footer = (): JSX.Element => ( -
    -
    -
    -
    - -
    -
    - - Home - - - Shipping - - - FAQ - -
    -
    - - About - - - Terms - - - Support - -
    -
    -
    - - {" "} - - - - {" "} - - - - - +const Footer = (): JSX.Element => { + return ( +
    +
    +
    +
    + +
    +
    + + Home + + + Shipping + + + FAQ + +
    +
    + + About + + + Terms + + + Support + +
    +
    +
    + + {" "} + + + + {" "} + + + + + +
    -
    -); + ); +}; export default Footer; diff --git a/examples/core/src/components/header/AccountMobileMenu.tsx b/examples/core/src/components/header/AccountMobileMenu.tsx index 54d2c710..caf3cbb9 100644 --- a/examples/core/src/components/header/AccountMobileMenu.tsx +++ b/examples/core/src/components/header/AccountMobileMenu.tsx @@ -1,6 +1,6 @@ "use client"; -import { usePathname } from "next/navigation"; -import Link from "next/link"; +import { useParams, usePathname } from "next/navigation"; +import { LocaleLink } from "../LocaleLink"; import { ArrowLeftOnRectangleIcon, ArrowRightOnRectangleIcon, @@ -9,7 +9,7 @@ import { UserCircleIcon, UserPlusIcon, } from "@heroicons/react/24/outline"; -import { logout } from "../../app/(auth)/actions"; +import { logout } from "../../app/[lang]/(auth)/actions"; import { SheetClose } from "../sheet/Sheet"; import { ButtonHTMLAttributes, forwardRef } from "react"; import { Slot } from "@radix-ui/react-slot"; @@ -21,6 +21,7 @@ export function AccountMobileMenu({ }: { account: AccountMemberResponse; }) { + const { lang } = useParams(); const pathname = usePathname(); const isAccountAuthed = !!account; @@ -35,13 +36,13 @@ export function AccountMobileMenu({ pathname={{ target: "/login", current: pathname }} asChild > - +
    @@ -51,10 +52,10 @@ export function AccountMobileMenu({ pathname={{ target: "/register", current: pathname }} asChild > - +
    @@ -68,10 +69,10 @@ export function AccountMobileMenu({ pathname={{ target: "/account/summary", current: pathname }} asChild > - +
    @@ -81,13 +82,13 @@ export function AccountMobileMenu({ pathname={{ target: "/account/orders", current: pathname }} asChild > - +
    @@ -97,32 +98,29 @@ export function AccountMobileMenu({ pathname={{ target: "/account/addresses", current: pathname }} asChild > - +
    - - - - - - + + logout(lang as string)} + > + +
    )} diff --git a/examples/core/src/components/header/Header.tsx b/examples/core/src/components/header/Header.tsx index 841bcec4..13960e54 100644 --- a/examples/core/src/components/header/Header.tsx +++ b/examples/core/src/components/header/Header.tsx @@ -1,21 +1,22 @@ import MobileNavBar from "./navigation/MobileNavBar"; import NavBar from "./navigation/NavBar"; -import Link from "next/link"; +import { LocaleLink } from "../LocaleLink"; import EpIcon from "../icons/ep-icon"; import { Suspense } from "react"; import { AccountMenu } from "./account/AccountMenu"; import { Cart } from "./navigation/Cart"; import { Skeleton } from "../skeleton/Skeleton"; +import { LocaleSelector } from "./locale/LocaleSelector"; -const Header = async () => { +const Header = async ({ lang }: { lang: string }) => { return (
    - + - +
    @@ -25,9 +26,12 @@ const Header = async () => {
    - +
    + +
    + }> - +
    diff --git a/examples/core/src/components/header/account/AccountMenu.tsx b/examples/core/src/components/header/account/AccountMenu.tsx index a64c91fa..a0759461 100644 --- a/examples/core/src/components/header/account/AccountMenu.tsx +++ b/examples/core/src/components/header/account/AccountMenu.tsx @@ -6,7 +6,7 @@ import { retrieveAccountMemberCredentials } from "../../../lib/retrieve-account- import { cookies } from "next/headers"; import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../lib/cookie-constants"; -export async function AccountMenu() { +export async function AccountMenu({ lang }: { lang: string }) { const client = createElasticPathClient(); const accountMemberCookie = retrieveAccountMemberCredentials( @@ -27,7 +27,7 @@ export async function AccountMenu() { } + accountSwitcher={} /> ); } diff --git a/examples/core/src/components/header/account/AccountPopover.tsx b/examples/core/src/components/header/account/AccountPopover.tsx index e2661e7e..df70dde3 100644 --- a/examples/core/src/components/header/account/AccountPopover.tsx +++ b/examples/core/src/components/header/account/AccountPopover.tsx @@ -1,8 +1,8 @@ "use client"; import { ReactNode, useState } from "react"; -import { usePathname } from "next/navigation"; -import { logout } from "../../../app/(auth)/actions"; +import { useParams, usePathname } from "next/navigation"; +import { logout } from "../../../app/[lang]/(auth)/actions"; import { ArrowLeftOnRectangleIcon, ArrowRightOnRectangleIcon, @@ -12,7 +12,7 @@ import { UserIcon, UserPlusIcon, } from "@heroicons/react/24/outline"; -import Link from "next/link"; +import { LocaleLink } from "../../LocaleLink"; import { AccountMemberResponse } from "@epcc-sdk/sdks-shopper"; import { retrieveAccountMemberCredentials } from "../../../lib/retrieve-account-member-credentials"; import { @@ -32,6 +32,7 @@ export function AccountPopover({ account?: AccountMemberResponse; accountMemberTokens?: ReturnType; }) { + const { lang } = useParams(); const pathname = usePathname(); const [open, setOpen] = useState(false); @@ -39,7 +40,7 @@ export function AccountPopover({ const isAccountAuthed = account !== undefined; function logoutAction() { - logout(); + logout(lang as string); setOpen(true); } @@ -63,13 +64,13 @@ export function AccountPopover({ pathname.startsWith("/login") && "font-semibold", )} > - +
    @@ -79,13 +80,13 @@ export function AccountPopover({ pathname.startsWith("/register") && "font-semibold", )} > - +
    @@ -99,13 +100,13 @@ export function AccountPopover({ pathname.startsWith("/summary") && "font-semibold", )} > - +
    @@ -115,13 +116,13 @@ export function AccountPopover({ pathname.startsWith("/orders") && "font-semibold", )} > - +
    @@ -131,30 +132,29 @@ export function AccountPopover({ pathname.startsWith("/addresses") && "font-semibold", )} > - +
    -
    - - - - + + +
    )} diff --git a/examples/core/src/components/header/account/AccountSwitcher.tsx b/examples/core/src/components/header/account/AccountSwitcher.tsx index 30f305c6..799c5c34 100644 --- a/examples/core/src/components/header/account/AccountSwitcher.tsx +++ b/examples/core/src/components/header/account/AccountSwitcher.tsx @@ -1,13 +1,13 @@ "use server"; -import { selectedAccount } from "../../../app/(auth)/actions"; +import { selectedAccount } from "../../../app/[lang]/(auth)/actions"; import { CheckCircleIcon, UserCircleIcon } from "@heroicons/react/24/outline"; import { cookies } from "next/headers"; import { retrieveAccountMemberCredentials } from "../../../lib/retrieve-account-member-credentials"; import { ACCOUNT_MEMBER_TOKEN_COOKIE_NAME } from "../../../lib/cookie-constants"; import { SwitchButton } from "./switch-button"; -export async function AccountSwitcher() { +export async function AccountSwitcher({ lang }: { lang: string }) { const cookieStore = await cookies(); const accountMemberCookie = retrieveAccountMemberCredentials( cookieStore, @@ -27,6 +27,7 @@ export async function AccountSwitcher() { selectedAccountId === value.account_id ? CheckCircleIcon : UserCircleIcon; return ( + void; + onCancel: () => void; +}) { + if (!open) return null; + + return ( +
    +
    +

    Change Language?

    +

    + Changing locale will empty your cart. Continue? +

    + +
    + + +
    +
    +
    + ); +} diff --git a/examples/core/src/components/header/locale/LocaleSelector.tsx b/examples/core/src/components/header/locale/LocaleSelector.tsx new file mode 100644 index 00000000..7b1854bb --- /dev/null +++ b/examples/core/src/components/header/locale/LocaleSelector.tsx @@ -0,0 +1,76 @@ +"use client"; +import { useRouter, usePathname } from "next/navigation"; +import { SUPPORTED_LOCALES } from "src/lib/i18n"; +import { ConfirmLocaleChangeModal } from "./ConfirmLocaleChangeModal"; +import { useState } from "react"; +import { removeAllCartItemsAction } from "src/app/[lang]/(store)/products/[productId]/actions/cart-actions"; +import { useNotify } from "src/hooks/use-event"; + +export const LocaleSelector = () => { + const router = useRouter(); + const pathname = usePathname(); + const notify = useNotify(); + const currentLocale = pathname.split("/")[1]; + const [pendingLocale, setPendingLocale] = useState(null); + const [showModal, setShowModal] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + const newLocale = e.target.value; + if (newLocale === currentLocale) return; + setPendingLocale(newLocale); + setShowModal(true); + }; + + const clearCart = async () => { + try { + const result = await removeAllCartItemsAction() + if (result.error) { + notify({ + scope: "cart", + type: "error", + action: "remove-cart-item", + message: (result.error as any).errors[0]?.detail, + cause: { + type: "cart-store-error", + cause: new Error(JSON.stringify(result.error)), + }, + }) + } else { + notify({ + scope: "cart", + type: "success", + action: "remove-cart-item", + message: "Successfully removed all cart items", + }) + } + } catch (e) { + console.error("Failed clearing cart", e); + } + }; + + const confirmChange = async () => { + if (!pendingLocale) return; + await clearCart(); + const segments = pathname.split("/"); + segments[1] = pendingLocale; + setShowModal(false); + router.push(segments.join("/")); + }; + + return ( + <> + + setShowModal(false)} + /> + + ); +}; \ No newline at end of file diff --git a/examples/core/src/components/header/navigation/Cart.tsx b/examples/core/src/components/header/navigation/Cart.tsx index d1fc7763..dea4e987 100644 --- a/examples/core/src/components/header/navigation/Cart.tsx +++ b/examples/core/src/components/header/navigation/Cart.tsx @@ -6,8 +6,9 @@ import { cookies } from "next/headers"; import { CART_COOKIE_NAME } from "../../../lib/cookie-constants"; import { getACart, getAllCurrencies, getByContextAllProducts } from "@epcc-sdk/sdks-shopper"; import { TAGS } from "../../../lib/constants"; +import { getPreferredCurrency } from "src/lib/i18n"; -export async function Cart() { +export async function Cart({ lang }: { lang: string }) { const client = createElasticPathClient(); const cartId = (await cookies()).get(CART_COOKIE_NAME)?.value; @@ -16,6 +17,14 @@ export async function Cart() { return null; } + const currencies = await getAllCurrencies({ + client, + next: { + tags: [TAGS.currencies], + }, + }); + const currency = await getPreferredCurrency(lang, currencies.data?.data || []); + const cartResponse = await getACart({ client, path: { @@ -27,8 +36,15 @@ export async function Cart() { next: { tags: [TAGS.cart], }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currency?.code + } }); + const cartCurrency = cartResponse.data?.data?.meta?.display_price?.with_tax?.currency; + const currencyUpdated = getPreferredCurrency(lang, currencies.data?.data || [], cartCurrency); + // Fetch product details for each cart item to get original sale price const cartItems = cartResponse?.data?.included?.items; const productIds = cartItems?.map(item => item.product_id).filter(Boolean); @@ -36,6 +52,10 @@ export async function Cart() { client, query: { filter: `in(id,${productIds?.join(",")})`, + }, + headers: { + "Accept-Language": lang, + "X-Moltin-Currency": currencyUpdated?.code, } }); const productDetails = productDetailsResponse.data?.data || []; @@ -49,14 +69,6 @@ export async function Cart() { }; }); - // Fetch currencies - const currencies = await getAllCurrencies({ - client, - next: { - tags: [TAGS.currencies], - }, - }); - if (!cartResponse.data) { console.error("No cart found"); return null; diff --git a/examples/core/src/components/header/navigation/MobileAccountSwitcher.tsx b/examples/core/src/components/header/navigation/MobileAccountSwitcher.tsx index 0624d143..f1536cbb 100644 --- a/examples/core/src/components/header/navigation/MobileAccountSwitcher.tsx +++ b/examples/core/src/components/header/navigation/MobileAccountSwitcher.tsx @@ -1,6 +1,6 @@ "use client"; -import { selectedAccount } from "../../../app/(auth)/actions"; +import { selectedAccount } from "../../../app/[lang]/(auth)/actions"; import { CheckCircleIcon, UserCircleIcon } from "@heroicons/react/24/outline"; import { SwitchButton } from "../account/switch-button"; import { Separator } from "../../separator/Separator"; @@ -9,6 +9,7 @@ import { getSelectedAccount, retrieveAccountMemberCredentials, } from "../../../lib/retrieve-account-member-credentials"; +import { useParams } from "next/navigation"; export function MobileAccountSwitcher({ account, @@ -19,6 +20,7 @@ export function MobileAccountSwitcher({ ReturnType >; }) { + const { lang } = useParams(); if (!account || !accountMemberTokens) { return null; } @@ -47,6 +49,7 @@ export function MobileAccountSwitcher({ : UserCircleIcon; return ( + - + - +
    }> diff --git a/examples/core/src/components/header/navigation/NavBarPopover.tsx b/examples/core/src/components/header/navigation/NavBarPopover.tsx index cb580196..59df4fe0 100644 --- a/examples/core/src/components/header/navigation/NavBarPopover.tsx +++ b/examples/core/src/components/header/navigation/NavBarPopover.tsx @@ -9,7 +9,7 @@ import { NavigationMenuList, NavigationMenuTrigger, } from "../../navigation-menu/NavigationMenu"; -import Link from "next/link"; +import { LocaleLink } from "src/components/LocaleLink"; import { ArrowRightIcon } from "@heroicons/react/20/solid"; export function NavBarPopover({ @@ -22,7 +22,7 @@ export function NavBarPopover({
    {item.name} {item.children.map((child: NavigationNode) => ( - {child.name} - + ))} - + Browse All - +
    ); }; @@ -63,7 +63,7 @@ export function NavBarPopover({ )}

    - - +
    diff --git a/examples/core/src/components/header/navigation/NavItemContent.tsx b/examples/core/src/components/header/navigation/NavItemContent.tsx index d9abb444..703f1cf5 100644 --- a/examples/core/src/components/header/navigation/NavItemContent.tsx +++ b/examples/core/src/components/header/navigation/NavItemContent.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { LocaleLink } from "../../LocaleLink"; import { NavigationNode } from "../../../lib/build-site-navigation"; import { ArrowRightIcon } from "@heroicons/react/20/solid"; @@ -15,7 +15,7 @@ const NavItemContent = ({ item, setOpen }: IProps): JSX.Element => {
    {item.name} {item.children.map((child: NavigationNode) => ( - setOpen && setOpen(false)} @@ -23,16 +23,16 @@ const NavItemContent = ({ item, setOpen }: IProps): JSX.Element => { className="hover:text-brand-primary hover:underline" > {child.name} - + ))} - setOpen && setOpen(false)} passHref className="hover:text-brand-primary hover:underline font-semibold" > Browse All - +
    ); }; @@ -45,7 +45,7 @@ const NavItemContent = ({ item, setOpen }: IProps): JSX.Element => { })}
    - setOpen && setOpen(false)} @@ -53,7 +53,7 @@ const NavItemContent = ({ item, setOpen }: IProps): JSX.Element => { > Browse All {item.name} - + ); }; diff --git a/examples/core/src/components/number-input/NumberInput.tsx b/examples/core/src/components/number-input/NumberInput.tsx index f3e43272..6c71f465 100644 --- a/examples/core/src/components/number-input/NumberInput.tsx +++ b/examples/core/src/components/number-input/NumberInput.tsx @@ -11,7 +11,7 @@ import { FormItem, FormMessage, } from "../form/Form"; -import { updateCartItemAction } from "../../app/(store)/products/[productId]/actions/cart-actions"; +import { updateCartItemAction } from "../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../lib/cookie-constants"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; @@ -20,11 +20,13 @@ import { useNotify } from "../../hooks/use-event"; import { useQueryClient } from "@tanstack/react-query"; import { cn } from "../../lib/cn"; import { Item } from "../../lib/group-cart-items"; +import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import type { JSX } from "react"; interface NumberInputProps { item: Item; + currency?: ResponseCurrency; } const quantitySchema = z.object({ @@ -39,9 +41,10 @@ const cartErrorOptions = { action: "update-cart-item", } as const; -export const NumberInput = ({ item }: NumberInputProps): JSX.Element => { +export const NumberInput = ({ item, currency }: NumberInputProps): JSX.Element => { const notify = useNotify(); const queryClient = useQueryClient(); + const currencyCode = currency?.code; const values = useMemo(() => { return { @@ -66,7 +69,7 @@ export const NumberInput = ({ item }: NumberInputProps): JSX.Element => { cartItemId: itemId, quantity, location, - }); + }, currencyCode); if (result.error) { notify({ diff --git a/examples/core/src/components/product/bundles/BundleProductForm.tsx b/examples/core/src/components/product/bundles/BundleProductForm.tsx index db5f4144..32f22cd6 100644 --- a/examples/core/src/components/product/bundles/BundleProductForm.tsx +++ b/examples/core/src/components/product/bundles/BundleProductForm.tsx @@ -1,13 +1,13 @@ "use client"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; -import { ProductData, StockLocations } from "@epcc-sdk/sdks-shopper"; +import { ProductData, ResponseCurrency, StockLocations } from "@epcc-sdk/sdks-shopper"; import { ReactNode, useMemo } from "react"; import { useNotify } from "../../../hooks/use-event"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form } from "../../form/Form"; -import { addToBundleAction } from "../../../app/(store)/products/[productId]/actions/cart-actions"; +import { addToBundleAction } from "../../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { useQueryClient } from "@tanstack/react-query"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../../lib/cookie-constants"; @@ -25,10 +25,12 @@ export function BundleProductForm({ product, locations, children, + currency, }: { product: ProductData; locations?: StockLocations; children: ReactNode; + currency?: ResponseCurrency; }) { const notify = useNotify(); const queryClient = useQueryClient(); @@ -56,7 +58,7 @@ export function BundleProductForm({ async function handleSubmit(data: z.infer) { try { - const result = await addToBundleAction(data); + const result = await addToBundleAction(data, currency?.code); if (result.error) { notify({ ...cartErrorOptions, diff --git a/examples/core/src/components/product/bundles/BundleProductProvider.tsx b/examples/core/src/components/product/bundles/BundleProductProvider.tsx index 1136dfc8..40ccc9cb 100644 --- a/examples/core/src/components/product/bundles/BundleProductProvider.tsx +++ b/examples/core/src/components/product/bundles/BundleProductProvider.tsx @@ -5,6 +5,7 @@ import { Location, Product, ProductData, + ResponseCurrency, StockResponse, } from "@epcc-sdk/sdks-shopper"; import { createContext, ReactNode, type JSX, useContext, useMemo } from "react"; @@ -22,6 +23,7 @@ export interface BundleProductProvider { inventory?: StockResponse; children: ReactNode; locations?: Location[]; + currency?: ResponseCurrency; } export interface BundleProductContextType { @@ -39,6 +41,7 @@ export function BundleProductProvider({ product: sourceProduct, children, locations, + currency, }: BundleProductProvider): JSX.Element { const productContext = useCreateShopperProductContext( sourceProduct, @@ -73,6 +76,7 @@ export function BundleProductProvider({ {children} diff --git a/examples/core/src/components/product/standard/SimpleProductForm.tsx b/examples/core/src/components/product/standard/SimpleProductForm.tsx index dbdb8ee0..639a801c 100644 --- a/examples/core/src/components/product/standard/SimpleProductForm.tsx +++ b/examples/core/src/components/product/standard/SimpleProductForm.tsx @@ -1,5 +1,5 @@ "use client"; -import { ProductData } from "@epcc-sdk/sdks-shopper"; +import { ProductData, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; import { type StockLocations } from "@epcc-sdk/sdks-shopper"; import { ReactNode } from "react"; @@ -8,10 +8,11 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form } from "../../form/Form"; -import { addToCartAction } from "../../../app/(store)/products/[productId]/actions/cart-actions"; +import { addToCartAction } from "../../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { useQueryClient } from "@tanstack/react-query"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../../lib/cookie-constants"; +import { useParams } from "next/navigation"; export const simpleProductSchema = z.object({ productId: z.string(), @@ -29,10 +30,12 @@ export function SimpleProductForm({ product, locations, children, + currency, }: { product: ProductData; locations?: StockLocations; children: ReactNode; + currency?: ResponseCurrency; }) { const notify = useNotify(); const queryClient = useQueryClient(); @@ -48,7 +51,7 @@ export function SimpleProductForm({ async function handleSubmit(data: z.infer) { try { - const result = await addToCartAction(data); + const result = await addToCartAction(data, currency?.code); if (result.error) { notify({ diff --git a/examples/core/src/components/product/standard/SimpleProductProvider.tsx b/examples/core/src/components/product/standard/SimpleProductProvider.tsx index e183da6c..d6904523 100644 --- a/examples/core/src/components/product/standard/SimpleProductProvider.tsx +++ b/examples/core/src/components/product/standard/SimpleProductProvider.tsx @@ -1,5 +1,5 @@ "use client"; -import { Location, ProductData, StockResponse } from "@epcc-sdk/sdks-shopper"; +import { Location, ProductData, ResponseCurrency, StockResponse } from "@epcc-sdk/sdks-shopper"; import { ReactNode, type JSX } from "react"; import { ShopperProductProvider, @@ -12,6 +12,7 @@ export interface SimpleProductProvider { inventory?: StockResponse; children: ReactNode; locations?: Location[]; + currency?: ResponseCurrency; } export function SimpleProductProvider({ @@ -19,6 +20,7 @@ export function SimpleProductProvider({ product, children, locations, + currency, }: SimpleProductProvider): JSX.Element { const productContext = useCreateShopperProductContext( product, @@ -31,6 +33,7 @@ export function SimpleProductProvider({ {children} diff --git a/examples/core/src/components/product/variations/ProductVariations.tsx b/examples/core/src/components/product/variations/ProductVariations.tsx index dd8a6ca1..d4e7583f 100644 --- a/examples/core/src/components/product/variations/ProductVariations.tsx +++ b/examples/core/src/components/product/variations/ProductVariations.tsx @@ -5,7 +5,7 @@ import clsx from "clsx"; import { SkuChangeOpacityWrapper } from "../SkuChangeOpacityWrapper"; import { useVariationProduct } from "./useVariationContext"; import { SkuChangingContext } from "../../../lib/sku-changing-context"; -import { useRouter } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { getSkuIdFromOptions } from "../../../lib/product-helper"; import { allVariationsHaveSelectedOption } from "./util/all-variations-have-selected-option"; @@ -17,6 +17,8 @@ const getSelectedOption = ( }; const ProductVariations = () => { + const { lang } = useParams(); + const { variations, variationsMatrix, @@ -44,7 +46,12 @@ const ProductVariations = () => { allVariationsHaveSelectedOption(selectedOptions, variations) ) { context?.setIsChangingSku(true); - router.replace(`/products/${selectedSkuId}`, { scroll: false }); + router.replace( + lang + ? `/${lang}/products/${selectedSkuId}` + : `/products/${selectedSkuId}`, + { scroll: false }, + ); context?.setIsChangingSku(false); } }, [ diff --git a/examples/core/src/components/product/variations/VariationProductForm.tsx b/examples/core/src/components/product/variations/VariationProductForm.tsx index 2b245b79..ec4a5f7e 100644 --- a/examples/core/src/components/product/variations/VariationProductForm.tsx +++ b/examples/core/src/components/product/variations/VariationProductForm.tsx @@ -1,13 +1,13 @@ import { ProductData } from "@epcc-sdk/sdks-shopper"; import { getACartQueryKey } from "@epcc-sdk/sdks-shopper/react-query"; -import type { StockLocations } from "@epcc-sdk/sdks-shopper"; +import type { ResponseCurrency, StockLocations } from "@epcc-sdk/sdks-shopper"; import { ReactNode } from "react"; import { useNotify } from "../../../hooks/use-event"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form } from "../../form/Form"; -import { addToCartAction } from "../../../app/(store)/products/[productId]/actions/cart-actions"; +import { addToCartAction } from "../../../app/[lang]/(store)/products/[productId]/actions/cart-actions"; import { useQueryClient } from "@tanstack/react-query"; import { getCookie } from "cookies-next/client"; import { CART_COOKIE_NAME } from "../../../lib/cookie-constants"; @@ -29,10 +29,12 @@ export function VariationProductForm({ product, locations, children, + currency, }: { product: ProductData; locations?: StockLocations; children: ReactNode; + currency?: ResponseCurrency; }) { const notify = useNotify(); const queryClient = useQueryClient(); @@ -48,7 +50,7 @@ export function VariationProductForm({ async function handleSubmit(data: z.infer) { try { - const result = await addToCartAction(data); + const result = await addToCartAction(data, currency?.code); if (result.error) { notify({ diff --git a/examples/core/src/components/product/variations/VariationProductProvider.tsx b/examples/core/src/components/product/variations/VariationProductProvider.tsx index 45652cd2..a2cad866 100644 --- a/examples/core/src/components/product/variations/VariationProductProvider.tsx +++ b/examples/core/src/components/product/variations/VariationProductProvider.tsx @@ -25,7 +25,7 @@ import { createEmptyOptionDict, mapOptionsToVariation, } from "./util/map-options-to-variations"; -import type { Location, ProductMeta } from "@epcc-sdk/sdks-shopper"; +import type { Location, ProductMeta, ResponseCurrency } from "@epcc-sdk/sdks-shopper"; export interface VariationProductProvider { product: ProductData; @@ -33,6 +33,7 @@ export interface VariationProductProvider { inventory?: StockResponse; children: ReactNode; locations?: Location[]; + currency?: ResponseCurrency; } export interface VariationProductContextType { @@ -56,6 +57,7 @@ export function VariationProductProvider({ children, parentProduct, locations, + currency, }: VariationProductProvider): JSX.Element { const productContext = useCreateShopperProductContext( sourceProduct, @@ -100,6 +102,7 @@ export function VariationProductProvider({ {children} diff --git a/examples/core/src/components/search/Hit.tsx b/examples/core/src/components/search/Hit.tsx index 58bac822..22193ebf 100644 --- a/examples/core/src/components/search/Hit.tsx +++ b/examples/core/src/components/search/Hit.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { LocaleLink } from "../LocaleLink"; import Price from "../product/Price"; import StrikePrice from "../product/StrikePrice"; import { EP_CURRENCY_CODE } from "../../lib/resolve-ep-currency-code"; @@ -26,7 +26,7 @@ export default function HitComponent({ return ( <> - +
    - +

    {hit.attributes?.name}

    - +
    {hit.attributes?.description} @@ -80,7 +80,7 @@ export default function HitComponent({
    - +
    ); } diff --git a/examples/core/src/components/search/MobileFilters.tsx b/examples/core/src/components/search/MobileFilters.tsx index 21b51a84..358a4e23 100644 --- a/examples/core/src/components/search/MobileFilters.tsx +++ b/examples/core/src/components/search/MobileFilters.tsx @@ -3,7 +3,7 @@ import { Dialog, Transition } from "@headlessui/react"; import { Dispatch, Fragment, SetStateAction, type JSX } from "react"; import { XMarkIcon } from "@heroicons/react/24/solid"; import NodeMenu from "./NodeMenu"; -import { useStore } from "../../app/(store)/StoreProvider"; +import { useStore } from "../../app/[lang]/(store)/StoreProvider"; interface IMobileFilters { lookup?: BreadcrumbLookup; diff --git a/examples/core/src/components/search/NodeMenu.tsx b/examples/core/src/components/search/NodeMenu.tsx index 9d0b0563..3a6b199e 100644 --- a/examples/core/src/components/search/NodeMenu.tsx +++ b/examples/core/src/components/search/NodeMenu.tsx @@ -1,6 +1,6 @@ import { clsx } from "clsx"; import { usePathname } from "next/navigation"; -import Link from "next/link"; +import { LocaleLink } from "../LocaleLink"; import type { JSX } from "react"; import { NavigationNode } from "../../lib/build-site-navigation"; @@ -34,7 +34,7 @@ function MenuItem({ item }: MenuItemProps): JSX.Element { activeItem && clsx("ais-HierarchicalMenu-item--selected"), )} > - {item.name} - + {activeItem && !!item.children?.length && (
    diff --git a/examples/core/src/components/search/SearchResults.tsx b/examples/core/src/components/search/SearchResults.tsx index 7a20d3ab..9f5fe587 100644 --- a/examples/core/src/components/search/SearchResults.tsx +++ b/examples/core/src/components/search/SearchResults.tsx @@ -8,8 +8,8 @@ import { BreadcrumbLookup } from "../../lib/types/breadcrumb-lookup"; import { buildBreadcrumbLookup } from "../../lib/build-breadcrumb-lookup"; import MobileFilters from "./MobileFilters"; import { ProductListData } from "@epcc-sdk/sdks-shopper"; -import { useStore } from "../../app/(store)/StoreProvider"; -import { useElasticPathClient } from "../../app/(store)/ClientProvider"; +import { useStore } from "../../app/[lang]/(store)/StoreProvider"; +import { useElasticPathClient } from "../../app/[lang]/(store)/ClientProvider"; import { ResourcePagination } from "../pagination/ResourcePagination"; interface ISearchResults { diff --git a/examples/core/src/lib/create-cookie-from-generate-token-response.ts b/examples/core/src/lib/create-cookie-from-generate-token-response.ts index dfe455b7..c576d21b 100644 --- a/examples/core/src/lib/create-cookie-from-generate-token-response.ts +++ b/examples/core/src/lib/create-cookie-from-generate-token-response.ts @@ -7,7 +7,7 @@ import { import { AccountMemberCredential, AccountMemberCredentials, -} from "../app/(auth)/account-member-credentials-schema"; +} from "../app/[lang]/(auth)/account-member-credentials-schema"; export type AccountMemberAuthResponse = NonNullable< Awaited>["data"] diff --git a/examples/core/src/lib/format-currency.tsx b/examples/core/src/lib/format-currency.tsx index 694a2b93..004443b1 100644 --- a/examples/core/src/lib/format-currency.tsx +++ b/examples/core/src/lib/format-currency.tsx @@ -7,11 +7,40 @@ export function formatCurrency( locals?: Parameters[0]; } = { locals: "en-US" }, ) { - const { decimal_places = 2, code } = currency; + const { + decimal_places = 2, + decimal_point = ".", + thousand_separator = ",", + format = "{price} {code}", + code, + } = currency; const resolvedAmount = amount / Math.pow(10, decimal_places); - return new Intl.NumberFormat(options.locals, { + const [integerPart, decimalPartRaw] = resolvedAmount + .toFixed(decimal_places) + .split("."); + + const integerWithGrouping = integerPart?.replace( + /\B(?=(\d{3})+(?!\d))/g, + thousand_separator + ); + + const formattedNumber = `${integerWithGrouping}${decimal_point}${decimalPartRaw}`; + + const isNegative = resolvedAmount < 0; + + const absoluteFormattedNumber = formattedNumber.replace(/^-/, ""); + + let finalFormatted = format + .replace("{price}", absoluteFormattedNumber) + .replace("{code}", code ?? ""); + + if (isNegative) { + finalFormatted = `-${finalFormatted}`; + } + + return finalFormatted || new Intl.NumberFormat(options.locals, { style: "currency", maximumFractionDigits: decimal_places, minimumFractionDigits: decimal_places, diff --git a/examples/core/src/lib/i18n.ts b/examples/core/src/lib/i18n.ts new file mode 100644 index 00000000..a3930fe6 --- /dev/null +++ b/examples/core/src/lib/i18n.ts @@ -0,0 +1,28 @@ +import { ResponseCurrency } from "@epcc-sdk/sdks-shopper"; + +export const SUPPORTED_LOCALES = ["en", "fr", "de", "es", "en-GB"]; + +export const LOCALE_TO_CURRENCY: Record = { + en: "USD", + fr: "EUR", + de: "EUR", + "en-GB": "GBP", +}; + +export function getPreferredCurrency(lang: string | undefined, currencies: ResponseCurrency[], cartCurrencyCode?: string) { + if (!currencies?.length) return undefined; + + const preferredCode = cartCurrencyCode + ? cartCurrencyCode + : lang + ? LOCALE_TO_CURRENCY[lang] + : undefined + + let currency = currencies.find((c: any) => c.code === preferredCode && c.enabled) + + if (!currency) { + currency = currencies.find((c: any) => c.default && c.enabled); + } + + return currency; +}; diff --git a/examples/core/src/lib/retrieve-account-member-credentials.ts b/examples/core/src/lib/retrieve-account-member-credentials.ts index 33fdee1c..39bbbaa0 100644 --- a/examples/core/src/lib/retrieve-account-member-credentials.ts +++ b/examples/core/src/lib/retrieve-account-member-credentials.ts @@ -3,7 +3,7 @@ import { AccountMemberCredential, AccountMemberCredentials, accountMemberCredentialsSchema, -} from "../app/(auth)/account-member-credentials-schema"; +} from "../app/[lang]/(auth)/account-member-credentials-schema"; export function getSelectedAccount( memberCredentials: AccountMemberCredentials, diff --git a/examples/core/src/middleware.ts b/examples/core/src/middleware.ts index a409e123..94753ddc 100644 --- a/examples/core/src/middleware.ts +++ b/examples/core/src/middleware.ts @@ -1,16 +1,33 @@ -import { NextFetchEvent, NextRequest } from "next/server"; +import { NextFetchEvent, NextRequest, NextResponse } from "next/server"; import { implicitAuthMiddleware } from "./lib/middleware/implicit-auth-middleware"; import { cartCookieMiddleware } from "./lib/middleware/cart-cookie-middleware"; import { composeMiddleware } from "./lib/middleware/run-middleware"; +import { SUPPORTED_LOCALES } from "./lib/i18n"; export const config = { matcher: ["/", "/((?!_next|api|favicon|configuration-error).*)"], }; export async function middleware(req: NextRequest, event: NextFetchEvent) { + const acceptLanguage = req.headers.get("accept-language"); + const firstLang = acceptLanguage?.split(",")[0] ?? ""; + const browserLocale = firstLang.split("-")[0] || "en"; + + const DEFAULT_LOCALE = browserLocale; + + const url = req.nextUrl.clone(); + const pathSegments = url.pathname.split("/").filter(Boolean); + + const locale = pathSegments[0] ?? ""; + + if (!SUPPORTED_LOCALES.includes(locale)) { + url.pathname = `/${DEFAULT_LOCALE}${url.pathname}`; + return NextResponse.redirect(url); + } + const runner = composeMiddleware([ implicitAuthMiddleware, cartCookieMiddleware, ]); return runner(req, event); -} +} \ No newline at end of file