diff --git a/.changeset/clever-fans-tickle.md b/.changeset/clever-fans-tickle.md deleted file mode 100644 index e2b548b6e1..0000000000 --- a/.changeset/clever-fans-tickle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@bigcommerce/catalyst-core": patch ---- - -Prepend locale for redirected urls in tests. -More info: https://github.com/amannn/next-intl/issues/1335 diff --git a/.changeset/heavy-toes-own.md b/.changeset/heavy-toes-own.md deleted file mode 100644 index ad850f915d..0000000000 --- a/.changeset/heavy-toes-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@bigcommerce/catalyst-core": patch ---- - -Add missing metadata in account settings page. diff --git a/.changeset/silent-rats-hear.md b/.changeset/silent-rats-hear.md new file mode 100644 index 0000000000..c83657bb38 --- /dev/null +++ b/.changeset/silent-rats-hear.md @@ -0,0 +1,9 @@ +--- +"@bigcommerce/catalyst-core": minor +--- + +Bump `next-intl` which includes [some minor changes and updated APIs]((https://next-intl-docs.vercel.app/blog/next-intl-3-22)): + ++ Use new `createNavigation` api. ++ Pass `locale` to redirects. ++ `setRequestLocale` is no longer unstable. \ No newline at end of file diff --git a/.changeset/thick-sloths-lick.md b/.changeset/thick-sloths-lick.md new file mode 100644 index 0000000000..2545af0834 --- /dev/null +++ b/.changeset/thick-sloths-lick.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": patch +--- + +remove unnecessary fields from create account form diff --git a/.changeset/translations-patch-53688cca.md b/.changeset/translations-patch-53688cca.md new file mode 100644 index 0000000000..ad17b2636a --- /dev/null +++ b/.changeset/translations-patch-53688cca.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": patch +--- + +Update translations. diff --git a/.env.example b/.env.example index b0f4ee4647..25a1548379 100644 --- a/.env.example +++ b/.env.example @@ -2,10 +2,6 @@ # The control panel URL is of the form `https://store-{hash}.mybigcommerce.com`. BIGCOMMERCE_STORE_HASH= -# The access token from a store-level API account. The only scope required to run Catalyst is Carts `manage`. -# See https://developer.bigcommerce.com/docs/start/authentication/api-accounts#store-level-api-accounts -BIGCOMMERCE_ACCESS_TOKEN= - # A bearer token that authorizes server-to-server requests to the GraphQL Storefront API # See https://developer.bigcommerce.com/docs/rest-authentication/tokens/customer-impersonation-token BIGCOMMERCE_CUSTOMER_IMPERSONATION_TOKEN= diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index d97c4bb023..53b9739640 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -14,6 +14,7 @@ env: TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} BIGCOMMERCE_STORE_HASH: ${{ secrets.BIGCOMMERCE_STORE_HASH }} BIGCOMMERCE_CUSTOMER_IMPERSONATION_TOKEN: ${{ secrets.BIGCOMMERCE_CUSTOMER_IMPERSONATION_TOKEN }} + BIGCOMMERCE_CHANNEL_ID: ${{ secrets.BIGCOMMERCE_CHANNEL_ID }} jobs: lint-typecheck: diff --git a/.github/workflows/changesets-release.yml b/.github/workflows/changesets-release.yml index 1de90fcd92..2f6f7f6938 100644 --- a/.github/workflows/changesets-release.yml +++ b/.github/workflows/changesets-release.yml @@ -29,6 +29,8 @@ jobs: - name: Build Packages run: pnpm --filter "./packages/**" build + env: + CLI_SEGMENT_WRITE_KEY: ${{ secrets.CLI_SEGMENT_WRITE_KEY }} - name: Create Release Pull Request or Publish to npm id: changesets diff --git a/.github/workflows/translations-changeset.yml b/.github/workflows/translations-changeset.yml new file mode 100644 index 0000000000..84cacf3a50 --- /dev/null +++ b/.github/workflows/translations-changeset.yml @@ -0,0 +1,51 @@ +name: Create translations patch + +on: + pull_request: + types: + - opened + branches: + - main + +jobs: + create-translations-patch: + if: github.actor == 'bc-svc-local' + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Use commit SHA for filename + id: generate-sha + run: | + short_sha=$(echo "${GITHUB_SHA}" | cut -c1-8) + echo "SHORT_SHA=$short_sha" >> $GITHUB_OUTPUT + + - name: Create a translations changeset + env: + SHORT_SHA: ${{ steps.generate-sha.outputs.SHORT_SHA }} + run: | + mkdir -p .changeset + echo "--- + \"@bigcommerce/catalyst-core\": patch + --- + + Update translations." > .changeset/translations-patch-$SHORT_SHA.md + + - name: Commit changeset + env: + SHORT_SHA: ${{ steps.generate-sha.outputs.SHORT_SHA }} + run: | + git config --global user.name 'bc-svc-local' + git config --global user.email 'bc-svc-local@users.noreply.github.com' + git add .changeset/translations-patch-$SHORT_SHA.md + git commit -m "chore(core): create translations patch" + + - name: Push changeset + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} HEAD:${{ github.event.pull_request.head.ref }} diff --git a/core/.env.example b/core/.env.example index b0f4ee4647..25a1548379 100644 --- a/core/.env.example +++ b/core/.env.example @@ -2,10 +2,6 @@ # The control panel URL is of the form `https://store-{hash}.mybigcommerce.com`. BIGCOMMERCE_STORE_HASH= -# The access token from a store-level API account. The only scope required to run Catalyst is Carts `manage`. -# See https://developer.bigcommerce.com/docs/start/authentication/api-accounts#store-level-api-accounts -BIGCOMMERCE_ACCESS_TOKEN= - # A bearer token that authorizes server-to-server requests to the GraphQL Storefront API # See https://developer.bigcommerce.com/docs/rest-authentication/tokens/customer-impersonation-token BIGCOMMERCE_CUSTOMER_IMPERSONATION_TOKEN= diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index d869619dcf..c7e8e20606 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,5 +1,75 @@ # Changelog +## 0.17.1 + +### Patch Changes + +- Updated dependencies [[`d4120d3`](https://github.com/bigcommerce/catalyst/commit/d4120d39c10398e842a7ebe14ada685ec8aae3a8)]: + - @bigcommerce/catalyst-client@0.11.0 + +## 0.17.0 + +### Minor Changes + +- [#1401](https://github.com/bigcommerce/catalyst/pull/1401) [`3095002`](https://github.com/bigcommerce/catalyst/commit/3095002d7a10b9c4058016076deb7a45fc8ae7bb) Thanks [@bookernath](https://github.com/bookernath)! - Add dynamic robots.txt from control panel settings + +### Patch Changes + +- [#1477](https://github.com/bigcommerce/catalyst/pull/1477) [`79e705f`](https://github.com/bigcommerce/catalyst/commit/79e705f151a733a811effed40757030aba6b6300) Thanks [@deini](https://github.com/deini)! - Breadcrumbs for top level category pages are no longer rendered + +- [#1467](https://github.com/bigcommerce/catalyst/pull/1467) [`e763a83`](https://github.com/bigcommerce/catalyst/commit/e763a83bcd4b8b5311586247291338eb65fbc476) Thanks [@deini](https://github.com/deini)! - Fixes an issue when a numeric product option set to a minimum <= 0 breaks the counter component. + +- [#1459](https://github.com/bigcommerce/catalyst/pull/1459) [`b4485c7`](https://github.com/bigcommerce/catalyst/commit/b4485c76de8c83546c68a7b50fcb7991603dbf6e) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Updates the with-routes middleware to fallback on locale based rewrite logic if the redirect is a dynamic entity redirect. + +- [#1469](https://github.com/bigcommerce/catalyst/pull/1469) [`8e9e7f3`](https://github.com/bigcommerce/catalyst/commit/8e9e7f3d40545004b080146b4dbb42f4ac7cf17c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fixes the product quantity reseting back to the previous value when adjusting the quantity fails. + +- [#1476](https://github.com/bigcommerce/catalyst/pull/1476) [`d47e3ac`](https://github.com/bigcommerce/catalyst/commit/d47e3aceb244713bc996287319357e6af3d865ed) Thanks [@deini](https://github.com/deini)! - adds an empty state to category pages + +- [#1458](https://github.com/bigcommerce/catalyst/pull/1458) [`3d67f8d`](https://github.com/bigcommerce/catalyst/commit/3d67f8d0d1776d747e9aa485b0b29a738eeacf3c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add no-store to mutations that are rate limited. + +- [#1453](https://github.com/bigcommerce/catalyst/pull/1453) [`1c8b042`](https://github.com/bigcommerce/catalyst/commit/1c8b04278074eb55358a5515f330a011de9561b5) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations. + +- Updated dependencies [[`2d1526a`](https://github.com/bigcommerce/catalyst/commit/2d1526a50402b2eb677abd55f19fb904234d1a84)]: + - @bigcommerce/catalyst-client@0.10.0 + +## 0.16.0 + +### Minor Changes + +- [#1410](https://github.com/bigcommerce/catalyst/pull/1410) [`53cca82`](https://github.com/bigcommerce/catalyst/commit/53cca82611272fc3be24505b7c6d5866f10c87fd) Thanks [@bookernath](https://github.com/bookernath)! - Move /reset page to /login/forgot-password in order to reduce top-level routes. + +- [#1384](https://github.com/bigcommerce/catalyst/pull/1384) [`17692ca`](https://github.com/bigcommerce/catalyst/commit/17692caa3ff9b25180359d8a020470ece3e589f6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Pass customer ip address into requests that don't rely on cached values. + +- [#1388](https://github.com/bigcommerce/catalyst/pull/1388) [`a309a4d`](https://github.com/bigcommerce/catalyst/commit/a309a4dd47083a58c998a4f6d169185177cca571) Thanks [@deini](https://github.com/deini)! - wraps header and footer in suspense boundaries + +### Patch Changes + +- [#1374](https://github.com/bigcommerce/catalyst/pull/1374) [`1f76f61`](https://github.com/bigcommerce/catalyst/commit/1f76f615b38bb41db770653bd8e7947cd6361b18) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Prepend locale for redirected urls in tests. + More info: https://github.com/amannn/next-intl/issues/1335 + +- [#1373](https://github.com/bigcommerce/catalyst/pull/1373) [`971033f`](https://github.com/bigcommerce/catalyst/commit/971033fc63181bad15aa46abb65b0d44501922c9) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add missing metadata in account settings page. + +- [#1370](https://github.com/bigcommerce/catalyst/pull/1370) [`655d518`](https://github.com/bigcommerce/catalyst/commit/655d518b2fd662614539467fff940b2b5ff78567) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations. + +- [#1446](https://github.com/bigcommerce/catalyst/pull/1446) [`ba4820b`](https://github.com/bigcommerce/catalyst/commit/ba4820bf6dd36d0155028ad3db094bd9745d5d94) Thanks [@deini](https://github.com/deini)! - Fixes a bug where product variant was not reliably being selected on PDP when using pre-selected options. + +- [#1391](https://github.com/bigcommerce/catalyst/pull/1391) [`4d64c31`](https://github.com/bigcommerce/catalyst/commit/4d64c31d4765dd72c81c1836b66aa1d7cb34b5f5) Thanks [@bookernath](https://github.com/bookernath)! - Get lossy image from API instead of setting param in code + +- [#1389](https://github.com/bigcommerce/catalyst/pull/1389) [`a4eaff6`](https://github.com/bigcommerce/catalyst/commit/a4eaff6bb2520f748630e24a6a28ca31cd2eb2c3) Thanks [@bookernath](https://github.com/bookernath)! - Add additional IP address header + +- [#1402](https://github.com/bigcommerce/catalyst/pull/1402) [`6e75ef5`](https://github.com/bigcommerce/catalyst/commit/6e75ef5097e0f3227c04ac0d9d7bbc484513bcce) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - fixing the problem with submitting the password change form + +- [#1407](https://github.com/bigcommerce/catalyst/pull/1407) [`ac9832f`](https://github.com/bigcommerce/catalyst/commit/ac9832fcc61f01413a5b8f101f5f27c53ca1fce5) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations. + +- [#1392](https://github.com/bigcommerce/catalyst/pull/1392) [`76227ac`](https://github.com/bigcommerce/catalyst/commit/76227ac06bb349f604f1d2d4a9b68e7d0869eba4) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations. + +- [#1424](https://github.com/bigcommerce/catalyst/pull/1424) [`4874add`](https://github.com/bigcommerce/catalyst/commit/4874addfbdde90ac45aa57c10767587ba4c50735) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations. + +- [#1445](https://github.com/bigcommerce/catalyst/pull/1445) [`ba3f513`](https://github.com/bigcommerce/catalyst/commit/ba3f513ac4242ce6883ad6ab635d38156a271ca9) Thanks [@deini](https://github.com/deini)! - Adds optimistic updates to all "Add to cart" buttons. This change makes the UI feel snappier and give quick feedback on user interaction. + +- Updated dependencies [[`17692ca`](https://github.com/bigcommerce/catalyst/commit/17692caa3ff9b25180359d8a020470ece3e589f6)]: + - @bigcommerce/catalyst-client@0.9.0 + ## 0.15.0 ### Minor Changes diff --git a/core/app/[locale]/(default)/(auth)/change-password/_actions/change-password.ts b/core/app/[locale]/(default)/(auth)/change-password/_actions/change-password.ts index 3b224c6719..c59383109d 100644 --- a/core/app/[locale]/(default)/(auth)/change-password/_actions/change-password.ts +++ b/core/app/[locale]/(default)/(auth)/change-password/_actions/change-password.ts @@ -54,6 +54,9 @@ export const changePassword = async (_previousState: unknown, formData: FormData newPassword: parsedData.newPassword, }, }, + fetchOptions: { + cache: 'no-store', + }, }); const result = response.data.customer.resetPassword; diff --git a/core/app/[locale]/(default)/(auth)/change-password/page.tsx b/core/app/[locale]/(default)/(auth)/change-password/page.tsx index fc6e97af27..939c58ea93 100644 --- a/core/app/[locale]/(default)/(auth)/change-password/page.tsx +++ b/core/app/[locale]/(default)/(auth)/change-password/page.tsx @@ -1,4 +1,4 @@ -import { useTranslations } from 'next-intl'; +import { useLocale, useTranslations } from 'next-intl'; import { getTranslations } from 'next-intl/server'; import { redirect } from '~/i18n/routing'; @@ -22,12 +22,13 @@ interface Props { export default function ChangePassword({ searchParams }: Props) { const t = useTranslations('ChangePassword'); + const locale = useLocale(); const customerId = searchParams.c; const customerToken = searchParams.t; if (!customerId || !customerToken) { - redirect('/login'); + redirect({ href: '/login', locale }); } if (customerId && customerToken) { diff --git a/core/app/[locale]/(default)/(auth)/login/_actions/login.ts b/core/app/[locale]/(default)/(auth)/login/_actions/login.ts index ec5830fdfb..5ae86e8d32 100644 --- a/core/app/[locale]/(default)/(auth)/login/_actions/login.ts +++ b/core/app/[locale]/(default)/(auth)/login/_actions/login.ts @@ -1,12 +1,15 @@ 'use server'; import { isRedirectError } from 'next/dist/client/components/redirect'; +import { getLocale } from 'next-intl/server'; import { Credentials, signIn } from '~/auth'; import { redirect } from '~/i18n/routing'; export const login = async (_previousState: unknown, formData: FormData) => { try { + const locale = await getLocale(); + const credentials = Credentials.parse({ email: formData.get('email'), password: formData.get('password'), @@ -19,7 +22,7 @@ export const login = async (_previousState: unknown, formData: FormData) => { redirect: false, }); - redirect('/account'); + redirect({ href: '/account', locale }); } catch (error: unknown) { // We need to throw this error to trigger the redirect as Next.js uses error boundaries to redirect. if (isRedirectError(error)) { diff --git a/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx b/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx index e8c8f4b739..c4d6f61864 100644 --- a/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx +++ b/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx @@ -121,9 +121,9 @@ export const LoginForm = () => { - {t('Form.resetPassword')} + {t('Form.forgotPassword')} diff --git a/core/app/[locale]/(default)/(auth)/reset/_actions/reset-password.ts b/core/app/[locale]/(default)/(auth)/login/forgot-password/_actions/reset-password.ts similarity index 93% rename from core/app/[locale]/(default)/(auth)/reset/_actions/reset-password.ts rename to core/app/[locale]/(default)/(auth)/login/forgot-password/_actions/reset-password.ts index 025ccd2a5a..79622ae50d 100644 --- a/core/app/[locale]/(default)/(auth)/reset/_actions/reset-password.ts +++ b/core/app/[locale]/(default)/(auth)/login/forgot-password/_actions/reset-password.ts @@ -37,7 +37,7 @@ export const resetPassword = async ({ path, reCaptchaToken, }: SubmitResetPasswordForm) => { - const t = await getTranslations('Reset'); + const t = await getTranslations('Login.ForgotPassword'); try { const parsedData = ResetPasswordSchema.parse({ @@ -53,6 +53,9 @@ export const resetPassword = async ({ }, ...(reCaptchaToken && { reCaptchaV2: { token: reCaptchaToken } }), }, + fetchOptions: { + cache: 'no-store', + }, }); const result = response.data.customer.requestResetPassword; diff --git a/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/fragment.ts b/core/app/[locale]/(default)/(auth)/login/forgot-password/_components/reset-password-form/fragment.ts similarity index 100% rename from core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/fragment.ts rename to core/app/[locale]/(default)/(auth)/login/forgot-password/_components/reset-password-form/fragment.ts diff --git a/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx b/core/app/[locale]/(default)/(auth)/login/forgot-password/_components/reset-password-form/index.tsx similarity index 97% rename from core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx rename to core/app/[locale]/(default)/(auth)/login/forgot-password/_components/reset-password-form/index.tsx index c1e662ec1c..b84e55abd4 100644 --- a/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx +++ b/core/app/[locale]/(default)/(auth)/login/forgot-password/_components/reset-password-form/index.tsx @@ -34,7 +34,7 @@ interface FormStatus { } const SubmitButton = () => { - const t = useTranslations('Reset.Form'); + const t = useTranslations('Login.ForgotPassword.Form'); const { pending } = useFormStatus(); @@ -52,7 +52,7 @@ const SubmitButton = () => { }; export const ResetPasswordForm = ({ reCaptchaSettings }: Props) => { - const t = useTranslations('Reset.Form'); + const t = useTranslations('Login.ForgotPassword.Form'); const form = useRef(null); const [formStatus, setFormStatus] = useState(null); diff --git a/core/app/[locale]/(default)/(auth)/reset/page.tsx b/core/app/[locale]/(default)/(auth)/login/forgot-password/page.tsx similarity index 90% rename from core/app/[locale]/(default)/(auth)/reset/page.tsx rename to core/app/[locale]/(default)/(auth)/login/forgot-password/page.tsx index 3495079b74..46ccd5a6ba 100644 --- a/core/app/[locale]/(default)/(auth)/reset/page.tsx +++ b/core/app/[locale]/(default)/(auth)/login/forgot-password/page.tsx @@ -24,7 +24,7 @@ const ResetPageQuery = graphql( ); export async function generateMetadata() { - const t = await getTranslations('Reset'); + const t = await getTranslations('Login.ForgotPassword'); return { title: t('title'), @@ -32,7 +32,7 @@ export async function generateMetadata() { } export default async function Reset() { - const t = await getTranslations('Reset'); + const t = await getTranslations('Login.ForgotPassword'); const { data } = await client.fetch({ document: ResetPageQuery, diff --git a/core/app/[locale]/(default)/(auth)/login/page.tsx b/core/app/[locale]/(default)/(auth)/login/page.tsx index 26fe533c02..2594487d75 100644 --- a/core/app/[locale]/(default)/(auth)/login/page.tsx +++ b/core/app/[locale]/(default)/(auth)/login/page.tsx @@ -1,5 +1,5 @@ import { useTranslations } from 'next-intl'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Link } from '~/components/link'; import { Button } from '~/components/ui/button'; @@ -20,7 +20,7 @@ interface Props { } export default function Login({ params: { locale } }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = useTranslations('Login'); diff --git a/core/app/[locale]/(default)/(auth)/register/_actions/login.ts b/core/app/[locale]/(default)/(auth)/register/_actions/login.ts index 4342822917..c48635b590 100644 --- a/core/app/[locale]/(default)/(auth)/register/_actions/login.ts +++ b/core/app/[locale]/(default)/(auth)/register/_actions/login.ts @@ -1,11 +1,14 @@ 'use server'; import { isRedirectError } from 'next/dist/client/components/redirect'; +import { getLocale } from 'next-intl/server'; import { Credentials, signIn } from '~/auth'; import { redirect } from '~/i18n/routing'; export const login = async (formData: FormData) => { + const locale = await getLocale(); + try { const credentials = Credentials.parse({ email: formData.get('customer-email'), @@ -19,7 +22,7 @@ export const login = async (formData: FormData) => { redirect: false, }); - redirect('/account'); + redirect({ href: '/account', locale }); } catch (error: unknown) { if (isRedirectError(error)) { throw error; diff --git a/core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts b/core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts index fb564facea..9207237435 100644 --- a/core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts +++ b/core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts @@ -43,7 +43,7 @@ interface RegisterCustomerForm { } const isRegisterCustomerInput = (data: unknown): data is RegisterCustomerInput => { - if (typeof data === 'object' && data !== null && 'email' in data && 'address' in data) { + if (typeof data === 'object' && data !== null && 'email' in data) { return true; } @@ -71,6 +71,9 @@ export const registerCustomer = async ({ formData, reCaptchaToken }: RegisterCus input: parsedData, ...(reCaptchaToken && { reCaptchaV2: { token: reCaptchaToken } }), }, + fetchOptions: { + cache: 'no-store', + }, }); const result = response.data.customer.registerCustomer; diff --git a/core/app/[locale]/(default)/(auth)/register/_components/register-customer-form.tsx b/core/app/[locale]/(default)/(auth)/register/_components/register-customer-form.tsx index b0fff75e1e..5a11dbd6e4 100644 --- a/core/app/[locale]/(default)/(auth)/register/_components/register-customer-form.tsx +++ b/core/app/[locale]/(default)/(auth)/register/_components/register-customer-form.tsx @@ -14,11 +14,11 @@ import { DateField, FieldNameToFieldId, FieldWrapper, + FULL_NAME_FIELDS, MultilineText, NumbersOnly, Password, Picklist, - PicklistOrText, RadioButtons, Text, } from '~/components/form-fields'; @@ -46,19 +46,10 @@ interface FormStatus { type CustomerFields = ExistingResultType['customerFields']; type AddressFields = ExistingResultType['addressFields']; -type Countries = ExistingResultType['countries']; -type CountryCode = Countries[number]['code']; -type CountryStates = Countries[number]['statesOrProvinces']; interface RegisterCustomerProps { addressFields: AddressFields; - countries: Countries; customerFields: CustomerFields; - defaultCountry: { - entityId: number; - code: CountryCode; - states: CountryStates; - }; reCaptchaSettings?: { isEnabledOnStorefront: boolean; siteKey: string; @@ -89,9 +80,7 @@ const SubmitButton = ({ messages }: SumbitMessages) => { export const RegisterCustomerForm = ({ addressFields, - countries, customerFields, - defaultCountry, reCaptchaSettings, }: RegisterCustomerProps) => { const form = useRef(null); @@ -104,7 +93,6 @@ export const RegisterCustomerForm = ({ }); const [numbersInputValid, setNumbersInputValid] = useState>({}); const [datesValid, setDatesValid] = useState>({}); - const [countryStates, setCountryStates] = useState(defaultCountry.states); const [radioButtonsValid, setRadioButtonsValid] = useState>({}); const [picklistValid, setPicklistValid] = useState>({}); const [checkboxesValid, setCheckboxesValid] = useState>({}); @@ -178,11 +166,6 @@ export const RegisterCustomerForm = ({ } }; - const handleCountryChange = (value: string) => { - const states = countries.find(({ code }) => code === value)?.statesOrProvinces; - - setCountryStates(states ?? []); - }; const handleRadioButtonsChange = createRadioButtonsValidationHandler( setRadioButtonsValid, radioButtonsValid, @@ -264,6 +247,27 @@ export const RegisterCustomerForm = ({ )}
+
+ {addressFields.map((field) => { + const fieldId = field.entityId; + const fieldName = createFieldName(field, 'customer'); + + if (field.__typename === 'TextFormField' && FULL_NAME_FIELDS.includes(fieldId)) { + return ( + + + + ); + } + + return null; + })} +
{customerFields .filter((field) => !CUSTOMER_FIELDS_TO_EXCLUDE.includes(field.entityId)) @@ -285,6 +289,18 @@ export const RegisterCustomerForm = ({ ); + case 'PasswordFormField': + return ( + + + + ); + case 'MultilineTextFormField': { return ( @@ -366,167 +382,10 @@ export const RegisterCustomerForm = ({ ); } - case 'PasswordFormField': { - return ( - - - - ); - } - default: return null; } })} -
-
- {addressFields.map((field) => { - const fieldId = field.entityId; - const fieldName = createFieldName(field, 'address'); - - switch (field.__typename) { - case 'TextFormField': { - return ( - - - - ); - } - - case 'MultilineTextFormField': { - return ( - - - - ); - } - - case 'NumberFormField': { - return ( - - - - ); - } - - case 'DateFormField': { - return ( - - - - ); - } - - case 'RadioButtonsFormField': { - return ( - - - - ); - } - - case 'PicklistFormField': { - const isCountrySelector = fieldId === FieldNameToFieldId.countryCode; - const picklistOptions = isCountrySelector - ? countries.map(({ name, code }) => ({ label: name, entityId: code })) - : field.options; - - return ( - - - - ); - } - - case 'CheckboxesFormField': { - return ( - - - - ); - } - - case 'PicklistOrTextFormField': { - return ( - - { - return { entityId: name, label: name }; - })} - /> - - ); - } - - case 'PasswordFormField': { - return ( - - - - ); - } - - default: - return null; - } - })} {reCaptchaSettings?.isEnabledOnStorefront && ( name === defaultCountry) || {}; + const { addressFields, customerFields, reCaptchaSettings } = registerCustomerData; return (

{t('heading')}

diff --git a/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx b/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx index 89bfa6f68d..21d43351c2 100644 --- a/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { ProductCard } from '~/components/product-card'; import { Pagination } from '~/components/ui/pagination'; @@ -40,7 +40,7 @@ export async function generateMetadata({ params }: Props): Promise { } export default async function Brand({ params: { slug, locale }, searchParams }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = await getTranslations('Brand'); diff --git a/core/app/[locale]/(default)/(faceted)/category/[slug]/_components/empty-state.tsx b/core/app/[locale]/(default)/(faceted)/category/[slug]/_components/empty-state.tsx new file mode 100644 index 0000000000..5a90a2997f --- /dev/null +++ b/core/app/[locale]/(default)/(faceted)/category/[slug]/_components/empty-state.tsx @@ -0,0 +1,22 @@ +import { PackageOpen } from 'lucide-react'; +import { useTranslations } from 'next-intl'; + +import { Link } from '~/components/link'; +import { Button } from '~/components/ui/button'; + +export const EmptyState = () => { + const t = useTranslations('Category.Empty'); + + return ( +
+ +

{t('message')}

+ +
+ +
+
+ ); +}; diff --git a/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx b/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx index 82ce252642..bb5457e3cd 100644 --- a/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Breadcrumbs } from '~/components/breadcrumbs'; import { ProductCard } from '~/components/product-card'; @@ -13,6 +13,7 @@ import { SortBy } from '../../_components/sort-by'; import { fetchFacetedSearch } from '../../fetch-faceted-search'; import { CategoryViewed } from './_components/category-viewed'; +import { EmptyState } from './_components/empty-state'; import { SubCategories } from './_components/sub-categories'; import { getCategoryPageData } from './page-data'; @@ -47,7 +48,7 @@ export async function generateMetadata({ params }: Props): Promise { } export default async function Category({ params: { locale, slug }, searchParams }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = await getTranslations('Category'); @@ -109,6 +110,8 @@ export default async function Category({ params: { locale, slug }, searchParams {t('products')} + {products.length === 0 && } +
{products.map((product, index) => ( {accountState.message}

)} + {!addressesCount &&

{t('emptyAddresses')}

}
    {addressBook.map( ({ diff --git a/core/app/[locale]/(default)/account/(tabs)/layout.tsx b/core/app/[locale]/(default)/account/(tabs)/layout.tsx index 1e03f7bc45..16efd38b34 100644 --- a/core/app/[locale]/(default)/account/(tabs)/layout.tsx +++ b/core/app/[locale]/(default)/account/(tabs)/layout.tsx @@ -1,5 +1,5 @@ import { useTranslations } from 'next-intl'; -import { unstable_setRequestLocale } from 'next-intl/server'; +import { setRequestLocale } from 'next-intl/server'; import { PropsWithChildren } from 'react'; import { Link } from '~/components/link'; @@ -15,7 +15,7 @@ interface Props extends PropsWithChildren { } export default function AccountTabLayout({ children, params: { locale } }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = useTranslations('Account.Home'); diff --git a/core/app/[locale]/(default)/account/(tabs)/settings/change-password/_components/change-password-form.tsx b/core/app/[locale]/(default)/account/(tabs)/settings/change-password/_components/change-password-form.tsx index d51b969633..add4bdeb44 100644 --- a/core/app/[locale]/(default)/account/(tabs)/settings/change-password/_components/change-password-form.tsx +++ b/core/app/[locale]/(default)/account/(tabs)/settings/change-password/_components/change-password-form.tsx @@ -132,32 +132,27 @@ export const ChangePasswordForm = () => { const handleCurrentPasswordChange = (e: ChangeEvent) => setIsCurrentPasswordValid(!e.target.validity.valueMissing); - const handleNewPasswordChange = (e: ChangeEvent) => { - let formData; - - if (e.target.form) { - formData = new FormData(e.target.form); - } - - const confirmPassword = formData?.get('confirm-password'); - const isValid = confirmPassword - ? validatePasswords('new-password', formData) && - validatePasswords('confirm-password', formData) - : validatePasswords('new-password', formData); - - setIsNewPasswordValid(isValid); + const validateNewAndConfirmPasswords = (formData: FormData) => { + const newPasswordValid = validatePasswords('new-password', formData); + const confirmPassword = formData.get('confirm-password'); + const confirmPasswordValid = confirmPassword + ? validatePasswords('confirm-password', formData) + : true; + + setIsNewPasswordValid(newPasswordValid); + setIsConfirmPasswordValid(confirmPasswordValid); }; - const handleConfirmPasswordValidation = (e: ChangeEvent) => { + const handlePasswordChange = (e: ChangeEvent) => { let formData; if (e.target.form) { formData = new FormData(e.target.form); } - const isValid = validatePasswords('confirm-password', formData); - - setIsConfirmPasswordValid(isValid); + if (formData) { + validateNewAndConfirmPasswords(formData); + } }; return ( @@ -200,8 +195,8 @@ export const ChangePasswordForm = () => { autoComplete="none" error={!isNewPasswordValid} id="new-password" - onChange={handleNewPasswordChange} - onInvalid={handleNewPasswordChange} + onChange={handlePasswordChange} + onInvalid={handlePasswordChange} required type="password" /> @@ -212,31 +207,11 @@ export const ChangePasswordForm = () => { > {t('notEmptyMessage')} - { - const currentPasswordValue = formData.get('current-password'); - const confirmPassword = formData.get('confirm-password'); - let isMatched; - - if (confirmPassword) { - isMatched = - newPasswordValue !== currentPasswordValue && newPasswordValue === confirmPassword; - - setIsNewPasswordValid(isMatched); - - return !isMatched; - } - - isMatched = currentPasswordValue === newPasswordValue; - - setIsNewPasswordValid(!isMatched); - - return isMatched; - }} - > - {t('newPasswordValidationMessage')} - + {!isNewPasswordValid && ( + + {t('newPasswordValidationMessage')} + + )} @@ -247,8 +222,8 @@ export const ChangePasswordForm = () => { autoComplete="none" error={!isConfirmPasswordValid} id="confirm-password" - onChange={handleConfirmPasswordValidation} - onInvalid={handleConfirmPasswordValidation} + onChange={handlePasswordChange} + onInvalid={handlePasswordChange} required type="password" /> @@ -259,19 +234,11 @@ export const ChangePasswordForm = () => { > {t('notEmptyMessage')} - { - const newPassword = formData.get('new-password'); - const isMatched = confirmPassword === newPassword; - - setIsConfirmPasswordValid(isMatched); - - return !isMatched; - }} - > - {t('confirmPasswordValidationMessage')} - + {!isConfirmPasswordValid && ( + + {t('confirmPasswordValidationMessage')} + + )}
    diff --git a/core/app/[locale]/(default)/account/(tabs)/settings/change-password/page.tsx b/core/app/[locale]/(default)/account/(tabs)/settings/change-password/page.tsx index 6be6a836b8..09bde16cde 100644 --- a/core/app/[locale]/(default)/account/(tabs)/settings/change-password/page.tsx +++ b/core/app/[locale]/(default)/account/(tabs)/settings/change-password/page.tsx @@ -1,4 +1,4 @@ -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { locales, LocaleType } from '~/i18n/routing'; @@ -19,7 +19,7 @@ interface Props { } export default function ChangePassword({ params: { locale } }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); return ( <> diff --git a/core/app/[locale]/(default)/account/layout.tsx b/core/app/[locale]/(default)/account/layout.tsx index 318dd8a6f5..27912e5e39 100644 --- a/core/app/[locale]/(default)/account/layout.tsx +++ b/core/app/[locale]/(default)/account/layout.tsx @@ -1,13 +1,15 @@ +import { getLocale } from 'next-intl/server'; import { PropsWithChildren } from 'react'; import { auth } from '~/auth'; import { redirect } from '~/i18n/routing'; export default async function AccountLayout({ children }: PropsWithChildren) { + const locale = await getLocale(); const session = await auth(); if (!session) { - redirect('/login'); + redirect({ href: '/login', locale }); } return children; diff --git a/core/app/[locale]/(default)/blog/[blogId]/_components/sharing-links.tsx b/core/app/[locale]/(default)/blog/[blogId]/_components/sharing-links.tsx index bc0720b75a..7ebfa30934 100644 --- a/core/app/[locale]/(default)/blog/[blogId]/_components/sharing-links.tsx +++ b/core/app/[locale]/(default)/blog/[blogId]/_components/sharing-links.tsx @@ -13,7 +13,7 @@ export const SharingLinksFragment = graphql(` post(entityId: $entityId) { entityId thumbnailImage { - url: urlTemplate + url: urlTemplate(lossy: true) } seo { pageTitle diff --git a/core/app/[locale]/(default)/blog/[blogId]/page-data.ts b/core/app/[locale]/(default)/blog/[blogId]/page-data.ts index 477011034e..6b25c564ed 100644 --- a/core/app/[locale]/(default)/blog/[blogId]/page-data.ts +++ b/core/app/[locale]/(default)/blog/[blogId]/page-data.ts @@ -22,7 +22,7 @@ const BlogPageQuery = graphql( tags thumbnailImage { altText - url: urlTemplate + url: urlTemplate(lossy: true) } seo { pageTitle diff --git a/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts b/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts index e3b129c600..85be76e95a 100644 --- a/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts +++ b/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts @@ -1,5 +1,6 @@ 'use server'; +import { getLocale } from 'next-intl/server'; import { z } from 'zod'; import { getSessionCustomerId } from '~/auth'; @@ -20,6 +21,7 @@ const CheckoutRedirectMutation = graphql(` `); export const redirectToCheckout = async (formData: FormData) => { + const locale = await getLocale(); const cartId = z.string().parse(formData.get('cartId')); const customerId = await getSessionCustomerId(); @@ -36,5 +38,5 @@ export const redirectToCheckout = async (formData: FormData) => { throw new Error('Invalid checkout url.'); } - redirect(url); + redirect({ href: url, locale }); }; diff --git a/core/app/[locale]/(default)/cart/_components/cart-item.tsx b/core/app/[locale]/(default)/cart/_components/cart-item.tsx index 0048fd035a..e853a233ee 100644 --- a/core/app/[locale]/(default)/cart/_components/cart-item.tsx +++ b/core/app/[locale]/(default)/cart/_components/cart-item.tsx @@ -12,7 +12,7 @@ const PhysicalItemFragment = graphql(` brand sku image { - url: urlTemplate + url: urlTemplate(lossy: true) } entityId quantity @@ -70,7 +70,7 @@ const DigitalItemFragment = graphql(` brand sku image { - url: urlTemplate + url: urlTemplate(lossy: true) } entityId quantity diff --git a/core/app/[locale]/(default)/cart/_components/item-quantity/index.tsx b/core/app/[locale]/(default)/cart/_components/item-quantity/index.tsx index 12277501f7..a7f2531044 100644 --- a/core/app/[locale]/(default)/cart/_components/item-quantity/index.tsx +++ b/core/app/[locale]/(default)/cart/_components/item-quantity/index.tsx @@ -171,6 +171,8 @@ export const ItemQuantity = ({ product }: { product: Product }) => { toast.error(t('errorMessage'), { icon: , }); + + setProductQuantity(quantity); } }; diff --git a/core/app/[locale]/(default)/compare/_components/add-to-cart/index.tsx b/core/app/[locale]/(default)/compare/_components/add-to-cart/index.tsx index 743ed9dffa..27deb4a88a 100644 --- a/core/app/[locale]/(default)/compare/_components/add-to-cart/index.tsx +++ b/core/app/[locale]/(default)/compare/_components/add-to-cart/index.tsx @@ -3,66 +3,72 @@ import { FragmentOf } from 'gql.tada'; import { AlertCircle, Check } from 'lucide-react'; import { useTranslations } from 'next-intl'; -import { useFormStatus } from 'react-dom'; +import { useTransition } from 'react'; import { toast } from 'react-hot-toast'; import { AddToCartButton } from '~/components/add-to-cart-button'; +import { useCart } from '~/components/header/cart-provider'; import { Link } from '~/components/link'; import { addToCart } from '../../_actions/add-to-cart'; import { AddToCartFragment } from './fragment'; -const Submit = ({ data: product }: { data: FragmentOf }) => { - const { pending } = useFormStatus(); - - return ; -}; - export const AddToCart = ({ data: product }: { data: FragmentOf }) => { const t = useTranslations('Compare.AddToCart'); + const cart = useCart(); + const [isPending, startTransition] = useTransition(); - return ( - { - const result = await addToCart(formData); - const quantity = Number(formData.get('quantity')); + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const quantity = Number(formData.get('quantity')); - if (result.error) { - toast.error(t('error'), { - icon: , - }); + // Optimistic update + cart.increment(quantity); + toast.success( + () => ( +
    + + {t.rich('success', { + cartItems: quantity, + cartLink: (chunks) => ( + + {chunks} + + ), + })} + +
    + ), + { icon: }, + ); - return; - } + startTransition(async () => { + const result = await addToCart(formData); - toast.success( - () => ( -
    - - {t.rich('success', { - cartItems: quantity, - cartLink: (chunks) => ( - - {chunks} - - ), - })} - -
    - ), - { icon: }, - ); - }} - > + if (result.error) { + cart.decrement(quantity); + + toast.error(t('error'), { + icon: , + }); + } + }); + }; + + return ( + - + + ); }; diff --git a/core/app/[locale]/(default)/compare/page.tsx b/core/app/[locale]/(default)/compare/page.tsx index a96b31362f..506e380252 100644 --- a/core/app/[locale]/(default)/compare/page.tsx +++ b/core/app/[locale]/(default)/compare/page.tsx @@ -51,7 +51,7 @@ const ComparePageQuery = graphql( } defaultImage { altText - url: urlTemplate + url: urlTemplate(lossy: true) } reviewSummary { numberOfReviews diff --git a/core/app/[locale]/(default)/layout.tsx b/core/app/[locale]/(default)/layout.tsx index d921d8d1bc..dbde593daa 100644 --- a/core/app/[locale]/(default)/layout.tsx +++ b/core/app/[locale]/(default)/layout.tsx @@ -1,52 +1,31 @@ -import { unstable_setRequestLocale } from 'next-intl/server'; -import { PropsWithChildren } from 'react'; +import { setRequestLocale } from 'next-intl/server'; +import { PropsWithChildren, Suspense } from 'react'; -import { getSessionCustomerId } from '~/auth'; -import { client } from '~/client'; -import { graphql } from '~/client/graphql'; -import { revalidate } from '~/client/revalidate-target'; import { Footer } from '~/components/footer/footer'; -import { FooterFragment } from '~/components/footer/fragment'; -import { Header } from '~/components/header'; +import { Header, HeaderSkeleton } from '~/components/header'; import { Cart } from '~/components/header/cart'; -import { HeaderFragment } from '~/components/header/fragment'; import { LocaleType } from '~/i18n/routing'; interface Props extends PropsWithChildren { params: { locale: LocaleType }; } -const LayoutQuery = graphql( - ` - query LayoutQuery { - site { - ...HeaderFragment - ...FooterFragment - } - } - `, - [HeaderFragment, FooterFragment], -); - -export default async function DefaultLayout({ children, params: { locale } }: Props) { - unstable_setRequestLocale(locale); - - const customerId = await getSessionCustomerId(); - - const { data } = await client.fetch({ - document: LayoutQuery, - fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } }, - }); +export default function DefaultLayout({ children, params: { locale } }: Props) { + setRequestLocale(locale); return ( <> -
    } data={data.site} /> + }> +
    } /> +
    {children}
    -
    + +
    + ); } diff --git a/core/app/[locale]/(default)/page.tsx b/core/app/[locale]/(default)/page.tsx index 70ace94387..13395b54bf 100644 --- a/core/app/[locale]/(default)/page.tsx +++ b/core/app/[locale]/(default)/page.tsx @@ -1,5 +1,5 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { getSessionCustomerId } from '~/auth'; import { client } from '~/client'; @@ -41,7 +41,7 @@ interface Props { } export default async function Home({ params: { locale } }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = await getTranslations('Home'); diff --git a/core/app/[locale]/(default)/product/[slug]/_components/gallery/fragment.ts b/core/app/[locale]/(default)/product/[slug]/_components/gallery/fragment.ts index c3dbb17cc0..7202b1ca5c 100644 --- a/core/app/[locale]/(default)/product/[slug]/_components/gallery/fragment.ts +++ b/core/app/[locale]/(default)/product/[slug]/_components/gallery/fragment.ts @@ -6,14 +6,14 @@ export const GalleryFragment = graphql(` edges { node { altText - url: urlTemplate + url: urlTemplate(lossy: true) isDefault } } } defaultImage { altText - url: urlTemplate + url: urlTemplate(lossy: true) } } `); diff --git a/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/fragment.ts b/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/fragment.ts index 8e393003a4..ae009edb3c 100644 --- a/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/fragment.ts +++ b/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/fragment.ts @@ -21,7 +21,7 @@ export const MultipleChoiceFieldFragment = graphql(` __typename defaultImage { altText - url: urlTemplate + url: urlTemplate(lossy: true) } } } diff --git a/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/number-field/index.tsx b/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/number-field/index.tsx index 89226298f9..4eba02dfaa 100644 --- a/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/number-field/index.tsx +++ b/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/number-field/index.tsx @@ -11,20 +11,23 @@ interface Props { } export const NumberField = ({ option }: Props) => { + const min = option.lowest !== null ? Number(option.lowest) : undefined; + const max = option.highest !== null ? Number(option.highest) : undefined; + const { field, fieldState } = useProductFieldController({ name: `attribute_${option.entityId}`, rules: { required: option.isRequired ? 'Please enter a number.' : false, - min: option.lowest + min: min ? { - value: option.lowest, - message: `Number must be equal or higher than ${option.lowest}.`, + value: min, + message: `Number must be equal or higher than ${min}.`, } : undefined, - max: option.highest + max: max ? { - value: option.highest, - message: `Number must be equal or lower than ${option.highest}.`, + value: max, + message: `Number must be equal or lower than ${max}.`, } : undefined, }, @@ -48,11 +51,11 @@ export const NumberField = ({ option }: Props) => { error={Boolean(error)} id={`${option.entityId}`} isInteger={option.isIntegerOnly} - max={Number(option.highest)} - min={Number(option.lowest)} + max={max} + min={min} name={field.name} onChange={field.onChange} - value={field.value ? Number(field.value) : ''} + value={field.value === '' ? '' : Number(field.value)} />
    {error && {error.message}} diff --git a/core/app/[locale]/(default)/product/[slug]/_components/product-form/index.tsx b/core/app/[locale]/(default)/product/[slug]/_components/product-form/index.tsx index eb0c8ddd9b..ed280a51ca 100644 --- a/core/app/[locale]/(default)/product/[slug]/_components/product-form/index.tsx +++ b/core/app/[locale]/(default)/product/[slug]/_components/product-form/index.tsx @@ -9,6 +9,7 @@ import { toast } from 'react-hot-toast'; import { ProductItemFragment } from '~/client/fragments/product-item'; import { AddToCartButton } from '~/components/add-to-cart-button'; +import { useCart } from '~/components/header/cart-provider'; import { Link } from '~/components/link'; import { Button } from '~/components/ui/button'; import { bodl } from '~/lib/bodl'; @@ -61,32 +62,14 @@ export const ProductForm = ({ data: product }: Props) => { const t = useTranslations('Product.Form'); const productOptions = removeEdgesAndNodes(product.productOptions); + const cart = useCart(); const { handleSubmit, register, ...methods } = useProductForm(); const productFormSubmit = async (data: ProductFormData) => { - const result = await handleAddToCart(data, product); const quantity = Number(data.quantity); - if (result.error) { - toast.error(t('error'), { - icon: , - }); - - return; - } - - const transformedProduct = productItemTransform(product); - - bodl.cart.productAdded({ - product_value: transformedProduct.purchase_price * quantity, - currency: transformedProduct.currency, - line_items: [ - { - ...transformedProduct, - quantity, - }, - ], - }); + // Optimistic update + cart.increment(quantity); toast.success( () => ( @@ -110,6 +93,31 @@ export const ProductForm = ({ data: product }: Props) => { ), { icon: }, ); + + const result = await handleAddToCart(data, product); + + if (result.error) { + toast.error(t('error'), { + icon: , + }); + + cart.decrement(quantity); + + return; + } + + const transformedProduct = productItemTransform(product); + + bodl.cart.productAdded({ + product_value: transformedProduct.purchase_price * quantity, + currency: transformedProduct.currency, + line_items: [ + { + ...transformedProduct, + quantity, + }, + ], + }); }; return ( diff --git a/core/app/[locale]/(default)/product/[slug]/_components/product-schema.tsx b/core/app/[locale]/(default)/product/[slug]/_components/product-schema.tsx index 4760f1a5cd..713029fba7 100644 --- a/core/app/[locale]/(default)/product/[slug]/_components/product-schema.tsx +++ b/core/app/[locale]/(default)/product/[slug]/_components/product-schema.tsx @@ -19,7 +19,7 @@ export const ProductSchemaFragment = graphql(` numberOfReviews } defaultImage { - url: urlTemplate + url: urlTemplate(lossy: true) } prices { price { diff --git a/core/app/[locale]/(default)/product/[slug]/page-data.ts b/core/app/[locale]/(default)/product/[slug]/page-data.ts index dd5e0d90b9..2d16108c15 100644 --- a/core/app/[locale]/(default)/product/[slug]/page-data.ts +++ b/core/app/[locale]/(default)/product/[slug]/page-data.ts @@ -33,7 +33,7 @@ const ProductPageQuery = graphql( entityId name defaultImage { - url: urlTemplate + url: urlTemplate(lossy: true) altText } categories(first: 1) { diff --git a/core/app/[locale]/(default)/product/[slug]/page.tsx b/core/app/[locale]/(default)/product/[slug]/page.tsx index fac994825d..ac519f13fb 100644 --- a/core/app/[locale]/(default)/product/[slug]/page.tsx +++ b/core/app/[locale]/(default)/product/[slug]/page.tsx @@ -1,7 +1,7 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { Metadata } from 'next'; import { notFound } from 'next/navigation'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Suspense } from 'react'; import { Breadcrumbs } from '~/components/breadcrumbs'; @@ -41,7 +41,7 @@ export async function generateMetadata({ params, searchParams }: Props): Promise const product = await getProduct({ entityId: productId, optionValueIds, - useDefaultOptionSelections: optionValueIds.length === 0 ? true : undefined, + useDefaultOptionSelections: true, }); if (!product) { @@ -69,7 +69,7 @@ export async function generateMetadata({ params, searchParams }: Props): Promise } export default async function Product({ params: { locale, slug }, searchParams }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = await getTranslations('Product'); @@ -80,7 +80,7 @@ export default async function Product({ params: { locale, slug }, searchParams } const product = await getProduct({ entityId: productId, optionValueIds, - useDefaultOptionSelections: optionValueIds.length === 0 ? true : undefined, + useDefaultOptionSelections: true, }); if (!product) { diff --git a/core/app/[locale]/(default)/query.ts b/core/app/[locale]/(default)/query.ts new file mode 100644 index 0000000000..713e2a109e --- /dev/null +++ b/core/app/[locale]/(default)/query.ts @@ -0,0 +1,15 @@ +import { graphql } from '~/client/graphql'; +import { FooterFragment } from '~/components/footer/fragment'; +import { HeaderFragment } from '~/components/header/fragment'; + +export const LayoutQuery = graphql( + ` + query LayoutQuery { + site { + ...HeaderFragment + ...FooterFragment + } + } + `, + [HeaderFragment, FooterFragment], +); diff --git a/core/app/[locale]/layout.tsx b/core/app/[locale]/layout.tsx index 8d30dc24ea..2f818fff1f 100644 --- a/core/app/[locale]/layout.tsx +++ b/core/app/[locale]/layout.tsx @@ -3,7 +3,7 @@ import { SpeedInsights } from '@vercel/speed-insights/next'; import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import { NextIntlClientProvider, useMessages } from 'next-intl'; -import { unstable_setRequestLocale } from 'next-intl/server'; +import { setRequestLocale } from 'next-intl/server'; import { PropsWithChildren } from 'react'; import '../globals.css'; @@ -82,8 +82,8 @@ interface Props extends PropsWithChildren { export default function RootLayout({ children, params: { locale } }: Props) { // need to call this method everywhere where static rendering is enabled - // https://next-intl-docs.vercel.app/docs/getting-started/app-router#add-unstable_setrequestlocale-to-all-layouts-and-pages - unstable_setRequestLocale(locale); + // https://next-intl-docs.vercel.app/docs/getting-started/app-router#add-setRequestLocale-to-all-layouts-and-pages + setRequestLocale(locale); const messages = useMessages(); diff --git a/core/app/[locale]/maintenance/page.tsx b/core/app/[locale]/maintenance/page.tsx index a983c25f28..e8b11dc303 100644 --- a/core/app/[locale]/maintenance/page.tsx +++ b/core/app/[locale]/maintenance/page.tsx @@ -1,5 +1,5 @@ import { Phone } from 'lucide-react'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { ReactNode } from 'react'; import { client } from '~/client'; @@ -42,7 +42,7 @@ interface Props { } export default async function Maintenance({ params: { locale } }: Props) { - unstable_setRequestLocale(locale); + setRequestLocale(locale); const t = await getTranslations('Maintenance'); diff --git a/core/app/[locale]/not-found.tsx b/core/app/[locale]/not-found.tsx index d131f294a1..2c6db7de04 100644 --- a/core/app/[locale]/not-found.tsx +++ b/core/app/[locale]/not-found.tsx @@ -1,15 +1,14 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { ShoppingCart } from 'lucide-react'; import { getTranslations } from 'next-intl/server'; +import { Suspense } from 'react'; import { client } from '~/client'; import { graphql } from '~/client/graphql'; import { revalidate } from '~/client/revalidate-target'; import { Footer } from '~/components/footer/footer'; -import { FooterFragment } from '~/components/footer/fragment'; -import { Header } from '~/components/header'; +import { Header, HeaderSkeleton } from '~/components/header'; import { CartLink } from '~/components/header/cart'; -import { HeaderFragment } from '~/components/header/fragment'; import { ProductCardFragment } from '~/components/product-card/fragment'; import { ProductCardCarousel } from '~/components/product-card-carousel'; import { SearchForm } from '~/components/search-form'; @@ -18,8 +17,6 @@ const NotFoundQuery = graphql( ` query NotFoundQuery { site { - ...HeaderFragment - ...FooterFragment featuredProducts(first: 4) { edges { node { @@ -30,7 +27,7 @@ const NotFoundQuery = graphql( } } `, - [HeaderFragment, FooterFragment, ProductCardFragment], + [ProductCardFragment], ); export default async function NotFound() { @@ -45,14 +42,15 @@ export default async function NotFound() { return ( <> -
    - - - } - data={data.site} - /> + }> +
    + + + } + /> +
    @@ -68,7 +66,9 @@ export default async function NotFound() { />
    -