diff --git a/app/components/product-form/product-media.tsx b/app/components/product-form/product-media.tsx index e8b5a10..44d884f 100644 --- a/app/components/product-form/product-media.tsx +++ b/app/components/product-form/product-media.tsx @@ -39,12 +39,12 @@ export function ProductMedia(props: ProductMediaProps) { }, }; - let [activeInd, setAcitveInd] = useState(0); - const [sliderRef, instanceRef] = useKeenSlider({ + let [activeInd, setActiveInd] = useState(0); + let [sliderRef, instanceRef] = useKeenSlider({ ...slideOptions, - slideChanged(slider) { + slideChanged: (slider) => { let pos = slider.track.details.rel; - setAcitveInd(pos); + setActiveInd(pos); let maxThumbnailIndex = thumbnailInstance.current?.track.details.maxIdx || 0; let thumbnailNext = Math.min( @@ -54,19 +54,18 @@ export function ProductMedia(props: ProductMediaProps) { thumbnailInstance.current?.moveToIdx(thumbnailNext); }, }); - let moveToIdx = useCallback( - (idx: number) => { - setAcitveInd(idx); - if (instanceRef.current) { - instanceRef.current.moveToIdx(idx); - } - }, - [instanceRef], - ); - const [thumbnailRef, thumbnailInstance] = useKeenSlider(thumbnailOptions); - let handleClickThumbnail = (idx: number) => { + + function moveToIdx(idx: number) { + setActiveInd(idx); + if (instanceRef.current) { + instanceRef.current.moveToIdx(idx); + } + } + + let [thumbnailRef, thumbnailInstance] = useKeenSlider(thumbnailOptions); + function handleClickThumbnail(idx: number) { moveToIdx(idx); - }; + } useEffect(() => { // instanceRef.current?.update(slideOptions); @@ -77,13 +76,12 @@ export function ProductMedia(props: ProductMediaProps) { }); moveToIdx(selectedInd); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedVariant?.id, moveToIdx]); + }, [selectedVariant?.id]); + return (
{media.map((med, i) => { diff --git a/app/routes/$.tsx b/app/routes/$.tsx deleted file mode 100644 index 1cd70df..0000000 --- a/app/routes/$.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export async function loader() { - throw new Response('Not found', {status: 404}); -} - -export default function Component() { - return null; -} diff --git a/app/routes/($locale).account.activate.$id.$activationToken.tsx b/app/routes/($locale).account.activate.$id.$activationToken.tsx deleted file mode 100644 index 018b0e9..0000000 --- a/app/routes/($locale).account.activate.$id.$activationToken.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import {json, redirect, type ActionFunction} from '@shopify/remix-oxygen'; -import {Form, useActionData, type MetaFunction} from '@remix-run/react'; -import {useRef, useState} from 'react'; - -import {getInputStyleClasses} from '~/lib/utils'; - -type ActionData = { - formError?: string; -}; - -const badRequest = (data: ActionData) => json(data, {status: 400}); - -export const handle = { - isPublic: true, -}; - -export const action: ActionFunction = async ({ - request, - context, - params: {locale, id, activationToken}, -}) => { - if ( - !id || - !activationToken || - typeof id !== 'string' || - typeof activationToken !== 'string' - ) { - return badRequest({ - formError: 'Wrong token. The link you followed might be wrong.', - }); - } - - const formData = await request.formData(); - - const password = formData.get('password'); - const passwordConfirm = formData.get('passwordConfirm'); - - if ( - !password || - !passwordConfirm || - typeof password !== 'string' || - typeof passwordConfirm !== 'string' || - password !== passwordConfirm - ) { - return badRequest({ - formError: 'Please provide matching passwords', - }); - } - - const {session, storefront} = context; - - try { - const data = await storefront.mutate(CUSTOMER_ACTIVATE_MUTATION, { - variables: { - id: `gid://shopify/Customer/${id}`, - input: { - password, - activationToken, - }, - }, - }); - - const {accessToken} = data?.customerActivate?.customerAccessToken ?? {}; - - if (!accessToken) { - /** - * Something is wrong with the user's input. - */ - throw new Error(data?.customerActivate?.customerUserErrors.join(', ')); - } - - session.set('customerAccessToken', accessToken); - - return redirect(locale ? `${locale}/account` : '/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); - } catch (error: any) { - if (storefront.isApiError(error)) { - return badRequest({ - formError: 'Something went wrong. Please try again later.', - }); - } - - /** - * The user did something wrong, but the raw error from the API is not super friendly. - * Let's make one up. - */ - return badRequest({ - formError: 'Sorry. We could not activate your account.', - }); - } -}; - -export const meta: MetaFunction = () => { - return [{title: 'Activate Account'}]; -}; - -export default function Activate() { - const actionData = useActionData(); - const [nativePasswordError, setNativePasswordError] = useState( - null, - ); - const [nativePasswordConfirmError, setNativePasswordConfirmError] = useState< - null | string - >(null); - - const passwordInput = useRef(null); - const passwordConfirmInput = useRef(null); - - const validatePasswordConfirm = () => { - if (!passwordConfirmInput.current) return; - - if ( - passwordConfirmInput.current.value.length && - passwordConfirmInput.current.value !== passwordInput.current?.value - ) { - setNativePasswordConfirmError('The two passwords entered did not match.'); - } else if ( - passwordConfirmInput.current.validity.valid || - !passwordConfirmInput.current.value.length - ) { - setNativePasswordConfirmError(null); - } else { - setNativePasswordConfirmError( - passwordConfirmInput.current.validity.valueMissing - ? 'Please re-enter the password' - : 'Passwords must be at least 8 characters', - ); - } - }; - - return ( -
-
-

Activate Account.

-

Create your password to activate your account.

- {/* TODO: Add onSubmit to validate _before_ submission with native? */} -
- {actionData?.formError && ( -
-

{actionData.formError}

-
- )} -
- { - if ( - event.currentTarget.validity.valid || - !event.currentTarget.value.length - ) { - setNativePasswordError(null); - validatePasswordConfirm(); - } else { - setNativePasswordError( - event.currentTarget.validity.valueMissing - ? 'Please enter a password' - : 'Passwords must be at least 8 characters', - ); - } - }} - /> - {nativePasswordError && ( -

- {' '} - {nativePasswordError}   -

- )} -
-
- - {nativePasswordConfirmError && ( -

- {' '} - {nativePasswordConfirmError}   -

- )} -
-
- -
-
-
-
- ); -} - -const CUSTOMER_ACTIVATE_MUTATION = `#graphql - mutation customerActivate($id: ID!, $input: CustomerActivateInput!) { - customerActivate(id: $id, input: $input) { - customerAccessToken { - accessToken - expiresAt - } - customerUserErrors { - code - field - message - } - } - } -`; diff --git a/app/routes/($locale).account.login.tsx b/app/routes/($locale).account.login.tsx deleted file mode 100644 index df65f7e..0000000 --- a/app/routes/($locale).account.login.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import {Form, useActionData, type MetaFunction} from '@remix-run/react'; -import { - json, - redirect, - type ActionFunction, - type AppLoadContext, - type LoaderFunctionArgs, -} from '@shopify/remix-oxygen'; -import type {MouseEvent} from 'react'; -import {useState} from 'react'; -import {Button, Input, Link} from '~/components'; - -export const handle = { - isPublic: true, -}; - -export async function loader({context, params}: LoaderFunctionArgs) { - const customerAccessToken = await context.session.get('customerAccessToken'); - - if (customerAccessToken) { - return redirect(params.locale ? `${params.locale}/account` : '/account'); - } - - // TODO: Query for this? - return json({shopName: 'Hydrogen'}); -} - -type ActionData = { - formError?: string; -}; - -const badRequest = (data: ActionData) => json(data, {status: 400}); - -export const action: ActionFunction = async ({request, context, params}) => { - const formData = await request.formData(); - - const email = formData.get('email'); - const password = formData.get('password'); - - if ( - !email || - !password || - typeof email !== 'string' || - typeof password !== 'string' - ) { - return badRequest({ - formError: 'Please provide both an email and a password.', - }); - } - - const {session, storefront, cart} = context; - - try { - const customerAccessToken = await doLogin(context, {email, password}); - session.set('customerAccessToken', customerAccessToken); - - // Sync customerAccessToken with existing cart - const result = await cart.updateBuyerIdentity({customerAccessToken}); - - // Update cart id in cookie - const headers = cart.setCartId(result.cart.id); - - headers.append('Set-Cookie', await session.commit()); - - return redirect(params.locale ? `/${params.locale}/account` : '/account', { - headers, - }); - } catch (error: any) { - if (storefront.isApiError(error)) { - return badRequest({ - formError: 'Something went wrong. Please try again later.', - }); - } - - /** - * The user did something wrong, but the raw error from the API is not super friendly. - * Let's make one up. - */ - return badRequest({ - formError: - 'Sorry. We did not recognize either your email or password. Please try to sign in again or create a new account.', - }); - } -}; - -export const meta: MetaFunction = () => { - return [{title: 'Login'}]; -}; - -export default function Login() { - const actionData = useActionData(); - const [nativeEmailError, setNativeEmailError] = useState(null); - const [nativePasswordError, setNativePasswordError] = useState( - null, - ); - - return ( -
-
-

Login

- {/* TODO: Add onSubmit to validate _before_ submission with native? */} -
- {actionData?.formError && ( -
-

{actionData.formError}

-
- )} -
-
- ) => { - setNativeEmailError( - event.currentTarget.value.length && - !event.currentTarget.validity.valid - ? 'Invalid email address' - : null, - ); - }} - /> - {nativeEmailError && ( -

- {nativeEmailError}   -

- )} -
- -
- ) => { - if ( - event.currentTarget.validity.valid || - !event.currentTarget.value.length - ) { - setNativePasswordError(null); - } else { - setNativePasswordError( - event.currentTarget.validity.valueMissing - ? 'Please enter a password' - : 'Passwords must be at least 8 characters', - ); - } - }} - /> - {nativePasswordError && ( -

- {' '} - {nativePasswordError}   -

- )} -
-
- - Forgot your password? - -
-
-
- - - Create an account - -
-
-
-
- ); -} - -const LOGIN_MUTATION = `#graphql - mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) { - customerAccessTokenCreate(input: $input) { - customerUserErrors { - code - field - message - } - customerAccessToken { - accessToken - expiresAt - } - } - } -`; - -export async function doLogin( - {storefront}: AppLoadContext, - { - email, - password, - }: { - email: string; - password: string; - }, -) { - const data = await storefront.mutate(LOGIN_MUTATION, { - variables: { - input: { - email, - password, - }, - }, - }); - - if (data?.customerAccessTokenCreate?.customerAccessToken?.accessToken) { - return data.customerAccessTokenCreate.customerAccessToken.accessToken; - } - - /** - * Something is wrong with the user's input. - */ - throw new Error( - data?.customerAccessTokenCreate?.customerUserErrors.join(', '), - ); -} diff --git a/app/routes/($locale).account.logout.ts b/app/routes/($locale).account.logout.ts deleted file mode 100644 index 414de9b..0000000 --- a/app/routes/($locale).account.logout.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - redirect, - type ActionFunction, - type AppLoadContext, - type LoaderFunctionArgs, - type ActionFunctionArgs, -} from '@shopify/remix-oxygen'; - -export async function doLogout(context: AppLoadContext) { - const {session} = context; - session.unset('customerAccessToken'); - - // The only file where I have to explicitly type cast i18n to pass typecheck - return redirect(`${context.storefront.i18n.pathPrefix}/account/login`, { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); -} - -export async function loader({context}: LoaderFunctionArgs) { - return redirect(context.storefront.i18n.pathPrefix); -} - -export const action: ActionFunction = async ({context}: ActionFunctionArgs) => { - return doLogout(context); -}; diff --git a/app/routes/($locale).account.recover.tsx b/app/routes/($locale).account.recover.tsx deleted file mode 100644 index 685ca07..0000000 --- a/app/routes/($locale).account.recover.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { - json, - redirect, - type ActionFunction, - type LoaderFunctionArgs, -} from '@shopify/remix-oxygen'; -import {Form, useActionData, type MetaFunction} from '@remix-run/react'; -import {useState, type MouseEvent} from 'react'; - -import {Button, Input, Link} from '~/components'; - -export async function loader({context, params}: LoaderFunctionArgs) { - const customerAccessToken = await context.session.get('customerAccessToken'); - - if (customerAccessToken) { - return redirect(params.locale ? `${params.locale}/account` : '/account'); - } - - return new Response(null); -} - -type ActionData = { - formError?: string; - resetRequested?: boolean; -}; - -const badRequest = (data: ActionData) => json(data, {status: 400}); - -export const action: ActionFunction = async ({request, context}) => { - const formData = await request.formData(); - const email = formData.get('email'); - - if (!email || typeof email !== 'string') { - return badRequest({ - formError: 'Please provide an email.', - }); - } - - try { - await context.storefront.mutate(CUSTOMER_RECOVER_MUTATION, { - variables: {email}, - }); - - return json({resetRequested: true}); - } catch (error: any) { - return badRequest({ - formError: 'Something went wrong. Please try again later.', - }); - } -}; - -export const meta: MetaFunction = () => { - return [{title: 'Recover Password'}]; -}; - -export default function Recover() { - const actionData = useActionData(); - const [nativeEmailError, setNativeEmailError] = useState(null); - const isSubmitted = actionData?.resetRequested; - - return ( -
-
- {isSubmitted ? ( - <> -

Request Sent.

-

- If that email address is in our system, you will receive an email - with instructions about how to reset your password in a few - minutes. -

- - ) : ( - <> -

Reset your password

-

- Enter your email to reset your password -

- {/* TODO: Add onSubmit to validate _before_ submission with native? */} -
- {actionData?.formError && ( -
-

- {actionData.formError} -

-
- )} -
- ) => { - setNativeEmailError( - event.currentTarget.value.length && - !event.currentTarget.validity.valid - ? 'Invalid email address' - : null, - ); - }} - /> - {nativeEmailError && ( -

- {nativeEmailError}   -

- )} -
-
- -
-

- Remember your password?   - - Back to Login - -

-
-
-
- - )} -
-
- ); -} - -const CUSTOMER_RECOVER_MUTATION = `#graphql - mutation customerRecover($email: String!) { - customerRecover(email: $email) { - customerUserErrors { - code - field - message - } - } - } -`; diff --git a/app/routes/($locale).account.register.tsx b/app/routes/($locale).account.register.tsx deleted file mode 100644 index 001640f..0000000 --- a/app/routes/($locale).account.register.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import {Form, useActionData, type MetaFunction} from '@remix-run/react'; -import { - json, - redirect, - type ActionFunction, - type LoaderFunctionArgs, -} from '@shopify/remix-oxygen'; -import {useState, type MouseEvent} from 'react'; - -import {Button, Input, Link} from '~/components'; - -import {doLogin} from './($locale).account.login'; - -export async function loader({context, params}: LoaderFunctionArgs) { - const customerAccessToken = await context.session.get('customerAccessToken'); - - if (customerAccessToken) { - return redirect(params.locale ? `${params.locale}/account` : '/account'); - } - - return new Response(null); -} - -type ActionData = { - formError?: string; -}; - -const badRequest = (data: ActionData) => json(data, {status: 400}); - -export const action: ActionFunction = async ({request, context, params}) => { - const {session, storefront} = context; - const formData = await request.formData(); - - const email = formData.get('email'); - const password = formData.get('password'); - - if ( - !email || - !password || - typeof email !== 'string' || - typeof password !== 'string' - ) { - return badRequest({ - formError: 'Please provide both an email and a password.', - }); - } - - try { - const data = await storefront.mutate(CUSTOMER_CREATE_MUTATION, { - variables: { - input: {email, password}, - }, - }); - - if (!data?.customerCreate?.customer?.id) { - /** - * Something is wrong with the user's input. - */ - throw new Error(data?.customerCreate?.customerUserErrors.join(', ')); - } - - const customerAccessToken = await doLogin(context, {email, password}); - session.set('customerAccessToken', customerAccessToken); - - return redirect(params.locale ? `${params.locale}/account` : '/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); - } catch (error: any) { - if (storefront.isApiError(error)) { - return badRequest({ - formError: 'Something went wrong. Please try again later.', - }); - } - - /** - * The user did something wrong, but the raw error from the API is not super friendly. - * Let's make one up. - */ - return badRequest({ - formError: - 'Sorry. We could not create an account with this email. User might already exist, try to login instead.', - }); - } -}; - -export const meta: MetaFunction = () => { - return [{title: 'Register'}]; -}; - -export default function Register() { - const actionData = useActionData(); - const [nativeEmailError, setNativeEmailError] = useState(null); - const [nativePasswordError, setNativePasswordError] = useState( - null, - ); - - return ( -
-
-

Create account

- {/* TODO: Add onSubmit to validate _before_ submission with native? */} -
- {actionData?.formError && ( -
-

{actionData.formError}

-
- )} -
-
- ) => { - setNativeEmailError( - event.currentTarget.value.length && - !event.currentTarget.validity.valid - ? 'Invalid email address' - : null, - ); - }} - /> - {nativeEmailError && ( -

- {nativeEmailError}   -

- )} -
-
- ) => { - if ( - event.currentTarget.validity.valid || - !event.currentTarget.value.length - ) { - setNativePasswordError(null); - } else { - setNativePasswordError( - event.currentTarget.validity.valueMissing - ? 'Please enter a password' - : 'Passwords must be at least 8 characters', - ); - } - }} - /> - {nativePasswordError && ( -

- {' '} - {nativePasswordError}   -

- )} -
-
-
- -
-

- Already have an account?   - - Log in here - -

-
-
-
-
-
- ); -} - -const CUSTOMER_CREATE_MUTATION = `#graphql - mutation customerCreate($input: CustomerCreateInput!) { - customerCreate(input: $input) { - customer { - id - } - customerUserErrors { - code - field - message - } - } - } -`; diff --git a/app/routes/($locale).account.reset.$id.$resetToken.tsx b/app/routes/($locale).account.reset.$id.$resetToken.tsx deleted file mode 100644 index a16ef41..0000000 --- a/app/routes/($locale).account.reset.$id.$resetToken.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import {json, redirect, type ActionFunction} from '@shopify/remix-oxygen'; -import {Form, useActionData, type MetaFunction} from '@remix-run/react'; -import {useRef, useState} from 'react'; - -import {getInputStyleClasses} from '~/lib/utils'; - -type ActionData = { - formError?: string; -}; - -const badRequest = (data: ActionData) => json(data, {status: 400}); - -export const action: ActionFunction = async ({ - request, - context, - params: {locale, id, resetToken}, -}) => { - if ( - !id || - !resetToken || - typeof id !== 'string' || - typeof resetToken !== 'string' - ) { - return badRequest({ - formError: 'Wrong token. Please try to reset your password again.', - }); - } - - const formData = await request.formData(); - - const password = formData.get('password'); - const passwordConfirm = formData.get('passwordConfirm'); - - if ( - !password || - !passwordConfirm || - typeof password !== 'string' || - typeof passwordConfirm !== 'string' || - password !== passwordConfirm - ) { - return badRequest({ - formError: 'Please provide matching passwords', - }); - } - - const {session, storefront} = context; - - try { - const data = await storefront.mutate(CUSTOMER_RESET_MUTATION, { - variables: { - id: `gid://shopify/Customer/${id}`, - input: { - password, - resetToken, - }, - }, - }); - - const {accessToken} = data?.customerReset?.customerAccessToken ?? {}; - - if (!accessToken) { - /** - * Something is wrong with the user's input. - */ - throw new Error(data?.customerReset?.customerUserErrors.join(', ')); - } - - session.set('customerAccessToken', accessToken); - - return redirect(locale ? `${locale}/account` : '/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); - } catch (error: any) { - if (storefront.isApiError(error)) { - return badRequest({ - formError: 'Something went wrong. Please try again later.', - }); - } - - /** - * The user did something wrong, but the raw error from the API is not super friendly. - * Let's make one up. - */ - return badRequest({ - formError: 'Sorry. We could not update your password.', - }); - } -}; - -export const meta: MetaFunction = () => { - return [{title: 'Reset Password'}]; -}; - -export default function Reset() { - const actionData = useActionData(); - const [nativePasswordError, setNativePasswordError] = useState( - null, - ); - const [nativePasswordConfirmError, setNativePasswordConfirmError] = useState< - null | string - >(null); - - const passwordInput = useRef(null); - const passwordConfirmInput = useRef(null); - - const validatePasswordConfirm = () => { - if (!passwordConfirmInput.current) return; - - if ( - passwordConfirmInput.current.value.length && - passwordConfirmInput.current.value !== passwordInput.current?.value - ) { - setNativePasswordConfirmError('The two passwords entered did not match.'); - } else if ( - passwordConfirmInput.current.validity.valid || - !passwordConfirmInput.current.value.length - ) { - setNativePasswordConfirmError(null); - } else { - setNativePasswordConfirmError( - passwordConfirmInput.current.validity.valueMissing - ? 'Please re-enter the password' - : 'Passwords must be at least 8 characters', - ); - } - }; - - return ( -
-
-

Reset Password.

-

Enter a new password for your account.

- {/* TODO: Add onSubmit to validate _before_ submission with native? */} -
- {actionData?.formError && ( -
-

{actionData.formError}

-
- )} -
- { - if ( - event.currentTarget.validity.valid || - !event.currentTarget.value.length - ) { - setNativePasswordError(null); - validatePasswordConfirm(); - } else { - setNativePasswordError( - event.currentTarget.validity.valueMissing - ? 'Please enter a password' - : 'Passwords must be at least 8 characters', - ); - } - }} - /> - {nativePasswordError && ( -

- {' '} - {nativePasswordError}   -

- )} -
-
- - {nativePasswordConfirmError && ( -

- {' '} - {nativePasswordConfirmError}   -

- )} -
-
- -
-
-
-
- ); -} - -const CUSTOMER_RESET_MUTATION = `#graphql - mutation customerReset($id: ID!, $input: CustomerResetInput!) { - customerReset(id: $id, input: $input) { - customerAccessToken { - accessToken - expiresAt - } - customerUserErrors { - code - field - message - } - } - } -`; diff --git a/app/sections/image-hotspots/items.tsx b/app/sections/image-hotspots/items.tsx index ca5ad78..3520392 100644 --- a/app/sections/image-hotspots/items.tsx +++ b/app/sections/image-hotspots/items.tsx @@ -11,7 +11,7 @@ import { ProductQuery } from 'storefrontapi.generated'; import { PRODUCT_QUERY } from '~/data/queries'; import clsx from 'clsx'; import { CSSProperties } from 'react'; -import { IconImageBlank } from '~/components'; +import { IconImageBlank, Link } from '~/components'; type ProductData = { @@ -27,25 +27,47 @@ type ProductsHotspotProps = HydrogenComponentProps< let ProductHotspotItems = forwardRef((props, ref) => { let { product, verticalPosition, horizontalPosition, loaderData, ...rest } = props; - const ProductImage = loaderData?.product?.variants.nodes.map((variant) => variant.image); - const ProductPrice = loaderData?.product?.variants.nodes.map((variant) => variant.price.amount) || '0.00'; - const ProductCurrency = loaderData?.product?.variants.nodes.map((variant) => variant.price.currencyCode) || '$'; - const ProductTittle = loaderData?.product?.title || 'Product title'; - console.log(loaderData?.product); let Horizontal = horizontalPosition >= 50 ? 'left-auto right-1/2' : 'right-auto left-1/2'; let Vertical = verticalPosition >= 50 ? 'top-auto bottom-full' : 'bottom-auto top-full'; + let sectionStyle: CSSProperties = { left: `${horizontalPosition}%`, top: `${verticalPosition}%`, } as CSSProperties; + + if (!loaderData) { + return ( +
+ + + +
+
+ +
+
+

Product title

+

0.00 $

+

Please select product

+
+
+
+ ); + } + + const ProductImage = loaderData?.product?.variants.nodes.map((variant) => variant.image); + const ProductPrice = loaderData?.product?.variants.nodes.map((variant) => variant.price.amount) || '0.00'; + const ProductCurrency = loaderData?.product?.variants.nodes.map((variant) => variant.price.currencyCode) || '$'; + const ProductTittle = loaderData?.product?.title || 'Product title'; + return (
-
+
{ProductImage ? ProductImage.map((image, index) => ( ((prop

{ProductTittle}

{`${ProductPrice} ${ProductCurrency}`}

+ {product.handle && See details}
diff --git a/app/sections/shared/Description.tsx b/app/sections/shared/Description.tsx index 73ebc82..a24330c 100644 --- a/app/sections/shared/Description.tsx +++ b/app/sections/shared/Description.tsx @@ -44,8 +44,8 @@ let Description = forwardRef< alignmentClasses[alignment!], className, )} + dangerouslySetInnerHTML={{__html: content}} > - {content} ); }); diff --git a/tests/utils.ts b/tests/utils.ts index 4e13aef..a5c5399 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -31,5 +31,3 @@ export function normalizePrice(price: string | null) { .replace('-', '.'), ); } - -