Skip to content
This repository was archived by the owner on Jan 19, 2024. It is now read-only.

Yotpo integration #83

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ NEXT_PUBLIC_DEMO_PROMO_ID=
NEXT_PUBLIC_DEMO_NODE_ID=
NEXT_PUBLIC_DEFAULT_CURRENCY_CODE=<required>
NEXT_PUBLIC_STRIPE_ACCOUNT_ID=<required>
NEXT_PUBLIC_ENABLE_RATING=
NEXT_PUBLIC_ALGOLIA_AVG_RATING_FIELD=ep_average_rating
NEXT_PUBLIC_ALGOLIA_REVIEW_COUNT_FIELD=ep_number_of_reviews
NEXT_PUBLIC_ENABLE_YOTPO=
NEXT_PUBLIC_YOTPO_APP_KEY=
NEXT_PUBLIC_DISABLE_IMAGE_OPTIMIZATION=
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ Add `NEXT_PUBLIC_STRIPE_ACCOUNT_ID` and `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` val
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
- Please contact Elastic Path Customer Service team to get the publishable key.

### Setup Yotpo (Ratings and Reviews)

Add `NEXT_PUBLIC_ENABLE_YOTPO` and `NEXT_PUBLIC_YOTPO_APP_KEY` value in your environment file.

- NEXT_PUBLIC_ENABLE_YOTPO
- Default value is false (even if you don't provide any value). To enable this integration you need to provide value as `true`
- NEXT_PUBLIC_YOTPO_APP_KEY
- You need to register with Yotpo, there are free trials available. Once you register, go to Settings and then you will able to see App Key. Copy app key from Yotpo and provide value here. Screenshot given below.

### Disable Image Optimization

By default image optimization is enabled. If you want to disable that then add value as `true` for this property in the environment variable `NEXT_PUBLIC_DISABLE_IMAGE_OPTIMIZATION`.

### Setup Algolia index

> :tired_face: We recognise manually configuring Algolia in this way is a pain. We are working on tools to streamline this process.
Expand Down Expand Up @@ -131,6 +144,8 @@ ep_extensions_products_specifications.on-sale
ep_extensions_products_specifications.color
```

For ratings filter, you need to add rating field as filter. If you are using default field value of `NEXT_PUBLIC_ALGOLIA_AVG_RATING_FIELD` as `ep_average_rating` then you need to select that.

In order to use faceting add the attribute name to the list of attributes in Facets and Facet display sections of Algolia dashboard in Configuration menu.
Use default settings.

Expand All @@ -145,6 +160,16 @@ my_catalog_index_price_desc

Follow ["Creating a replica"](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/#using-the-dashboard) in the Algolia docs to set both of these up based on the main index created previously by the integrations hub Aloglia integration. Make sure to create a **standard** replica.

##### Configuring rating and review

Add `NEXT_PUBLIC_ENABLE_RATING`, `NEXT_PUBLIC_ALGOLIA_AVG_RATING_FIELD` and `NEXT_PUBLIC_ALGOLIA_REVIEW_COUNT_FIELD` value in your environment file.

- NEXT_PUBLIC_ENABLE_RATING
- Default value is false (even if you don't provide any value). To enable this integration you need to provide value as `true`. Once you enable this then you will be able to see ratings and review on search pages and also as filter.
- NEXT_PUBLIC_ALGOLIA_AVG_RATING_FIELD
- Default value is `ep_average_rating`, if you want to use any other field then change this value. This field is responsible for showing filter and also stars as part of ratings on search pages.
- Default value is `ep_number_of_reviews`, if you want to use any other field then change this value. This field is responsible for showing number of reviews in search pages.

#### Finally

Make sure you add the three required Algolia environment variables to your `.env.local` file for local dev and your production environment.
Expand Down
2 changes: 1 addition & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
const nextConfig = {
images: {
domains: ["files-eu.epusercontent.com"],
unoptimized: process.env.NEXT_PUBLIC_DISABLE_IMAGE_OPTIMIZATION == "true",
},
experimental: { images: { allowFutureImage: true } },
i18n: {
locales: ["en"],
defaultLocale: "en",
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@moltin/sdk": "^18.1.0",
"@stripe/react-stripe-js": "^1.11.0",
"@stripe/stripe-js": "^1.38.1",
"@types/react-star-ratings": "^2.3.0",
"@types/react-stripe-elements": "^6.0.6",
"algoliasearch": "^4.14.2",
"braintree-web": "^3.87.0",
Expand All @@ -40,10 +41,11 @@
"react": "^18.2.0",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
"sass": "^1.55.0",
"react-instantsearch-hooks-server": "6.38.1",
"react-instantsearch-hooks-web": "6.38.1",
"react-star-ratings": "^2.3.0",
"rxjs": "^7.5.7",
"sass": "^1.55.0",
"yup": "^1.0.0-beta.7",
"zlib": "^1.0.5"
},
Expand Down Expand Up @@ -71,7 +73,7 @@
"typescript": "4.7.4"
},
"resolutions": {
"@types/react": "17.0.2",
"@types/react-dom": "17.0.2"
"@types/react": "18.0.33",
"@types/react-dom": "18.0.11"
}
}
4 changes: 3 additions & 1 deletion src/components/featured-products/FeaturedProducts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { connectProductsWithMainImages } from "../../lib/product-util";
import { ArrowForwardIcon, ViewOffIcon } from "@chakra-ui/icons";
import { globalBaseWidth } from "../../styles/theme";
import { ChakraNextImage } from "../ChakraNextImage";
import { Ratings } from "../reviews/yotpo/Reviews";

interface IFeaturedProductsBaseProps {
title: string;
Expand Down Expand Up @@ -104,7 +105,7 @@ const FeaturedProducts = (props: IFeaturedProductsProps): JSX.Element => {
p={4}
flex={{ base: "100%", md: "50%", lg: "25%" }}
key={product.id}
href="/category"
href={`/products/${product.id}`}
>
<Box width="100%" maxW={64} textAlign="center">
{product.main_image?.link.href ? (
Expand Down Expand Up @@ -135,6 +136,7 @@ const FeaturedProducts = (props: IFeaturedProductsProps): JSX.Element => {
<Heading size="sm" p="2" fontWeight="semibold">
{product.attributes.name}
</Heading>
<Ratings product={product} displayFromProduct={true} />
<Heading size="sm">
{product.meta.display_price?.without_tax.formatted}
</Heading>
Expand Down
2 changes: 2 additions & 0 deletions src/components/product/ProductSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Box, Heading, Flex } from "@chakra-ui/react";
import type { ProductResponse } from "@moltin/sdk";
import { useContext } from "react";
import { changingSkuStyle, ProductContext } from "../../lib/product-util";
import { Ratings } from "../reviews/yotpo/Reviews";
import Price from "./Price";
import StrikePrice from "./StrikePrice";

Expand All @@ -25,6 +26,7 @@ const ProductSummary = ({ product }: IProductSummary): JSX.Element => {
>
{attributes.name}
</Heading>
<Ratings product={product} />
{display_price && (
<Flex alignItems="center">
<Price
Expand Down
117 changes: 117 additions & 0 deletions src/components/reviews/yotpo/Reviews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Grid } from "@chakra-ui/react";
import type { ProductResponse } from "@moltin/sdk";
import { useEffect } from "react";
import Script from "next/script";
import { yotpoEnv } from "../../../lib/resolve-yotpo-env";
import StarRatings from "react-star-ratings";

declare global {
interface Window {
yotpo?: {
refreshWidgets: Function;
};
}
}

interface IReviews {
product: ProductResponse;
}

interface IRatings {
product: ProductResponse;
displayFromProduct?: boolean;
}

const Reviews = ({ product }: IReviews): JSX.Element => {
useEffect(() => {
if (yotpoEnv.enable && typeof window.yotpo !== "undefined") {
window.yotpo.refreshWidgets();
}
}, [product.id]);

const {
id,
attributes,
meta: { display_price },
} = product;

return yotpoEnv.enable ? (
<Grid>
<Script
id="yotpo-reviews"
src={`//staticw2.yotpo.com/${yotpoEnv.appKey}/widget.js`}
/>
<div
className="yotpo yotpo-main-widget"
style={{ marginTop: "20px" }}
data-product-id={id}
data-price={
display_price?.without_tax.amount
? display_price?.without_tax.amount / 100
: 0
}
data-currency={display_price?.without_tax.currency}
data-name={attributes.name}
></div>
</Grid>
) : (
<></>
);
};

const Ratings = ({ product, displayFromProduct }: IRatings): JSX.Element => {
useEffect(() => {
if (yotpoEnv.enable && typeof window.yotpo !== "undefined") {
window.yotpo.refreshWidgets();
}
}, [product.id]);

if (displayFromProduct) {
return yotpoEnv.enable ? (
<Grid
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
mt="1"
noOfLines={6}
margin={2}
>
<>
<StarRatings
rating={Number(
product.attributes.extensions?.["products(ratings)"]
?.average_rating || ""
)}
starDimension="18px"
starSpacing="0px"
starRatedColor="orange"
/>{" "}
(
{product.attributes.extensions?.["products(ratings)"]?.review_count ||
0}
)
</>
</Grid>
) : (
<></>
);
}
return yotpoEnv.enable ? (
<Grid marginTop={5}>
<Script
id="yotpo-reviews"
src={`//staticw2.yotpo.com/${yotpoEnv.appKey}/widget.js`}
/>
<div
className="yotpo bottomLine"
data-yotpo-product-id={product.id}
></div>
</Grid>
) : (
<></>
);
};

export default Reviews;
export { Ratings };
24 changes: 24 additions & 0 deletions src/components/search/Hit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import Price from "../product/Price";
import StrikePrice from "../product/StrikePrice";
import { ProductModalContainer } from "../product-modal/ProductModalContainer";
import { EP_CURRENCY_CODE } from "../../lib/resolve-ep-currency-code";
import { reviewsEnv } from "../../lib/resolve-reviews-field-env";
import StarRatings from "react-star-ratings";

export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element {
const { ep_price, ep_name, objectID, ep_main_image_url, ep_description } =
Expand Down Expand Up @@ -67,6 +69,28 @@ export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element {
>
{ep_description}
</Text>
<>
{reviewsEnv.enable && (
<Grid
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
mt="1"
noOfLines={6}
>
<>
<StarRatings
rating={Number(hit[reviewsEnv.avgRatingField || ""] || 0)}
starDimension="18px"
starSpacing="0px"
starRatedColor="orange"
/>
({hit[reviewsEnv.reviewCountField || ""] || 0})
</>
</Grid>
)}
</>
{currencyPrice && (
<Flex alignItems="center" mt="1">
<Price
Expand Down
6 changes: 6 additions & 0 deletions src/components/search/SearchHit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BaseHit } from "instantsearch.js";
import { reviewsEnv } from "../../lib/resolve-reviews-field-env";

type HitSalePrice = {
amount: number;
Expand Down Expand Up @@ -31,6 +32,9 @@ type HitPrice = {
};
};

const avgRatingField: unique symbol = Symbol(reviewsEnv.avgRatingField);
const reviewCountField: unique symbol = Symbol(reviewsEnv.reviewCountField);

export interface SearchHit extends BaseHit {
ep_amount: number;
ep_categories: string[];
Expand All @@ -42,4 +46,6 @@ export interface SearchHit extends BaseHit {
ep_main_image_url: string;
ep_image_url: string;
objectID: string;
[avgRatingField]?: string;
[reviewCountField]?: string;
}
31 changes: 26 additions & 5 deletions src/components/search/SearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { searchClient } from "../../lib/search-client";
import { algoliaEnvData } from "../../lib/resolve-algolia-env";
import { useDebouncedEffect } from "../../lib/use-debounced";
import { EP_CURRENCY_CODE } from "../../lib/resolve-ep-currency-code";
import { reviewsEnv } from "../../lib/resolve-reviews-field-env";
import StarRatings from "react-star-ratings";

const SearchBox = ({
onChange,
Expand Down Expand Up @@ -101,8 +103,7 @@ const SearchBox = ({
};

const HitComponent = ({ hit }: { hit: SearchHit }) => {
const { ep_price, ep_main_image_url, ep_name, ep_sku, ep_slug, objectID } =
hit;
const { ep_price, ep_main_image_url, ep_name, ep_sku, objectID } = hit;

const currencyPrice = ep_price?.[EP_CURRENCY_CODE];

Expand All @@ -128,9 +129,7 @@ const HitComponent = ({ hit }: { hit: SearchHit }) => {
</GridItem>
<GridItem colSpan={4}>
<Heading size="sm">
<LinkOverlay href={`/products/${ep_slug}/${objectID}`}>
{ep_name}
</LinkOverlay>
<LinkOverlay href={`/products/${objectID}`}>{ep_name}</LinkOverlay>
</Heading>
</GridItem>
<GridItem colSpan={4}>
Expand All @@ -143,6 +142,28 @@ const HitComponent = ({ hit }: { hit: SearchHit }) => {
>
{ep_sku}
</Text>
<>
{reviewsEnv.enable && (
<Grid
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
mt="1"
noOfLines={6}
>
<>
<StarRatings
rating={Number(hit[reviewsEnv.avgRatingField || ""] || 0)}
starDimension="18px"
starSpacing="0px"
starRatedColor="orange"
/>
({hit[reviewsEnv.reviewCountField || ""] || 0})
</>
</Grid>
)}
</>
</GridItem>
<GridItem colSpan={2}>
{currencyPrice && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import ColorRefinement from "./ColorRefinement";
import BrandRefinement from "./BrandRefirement";
import OnSaleRefinement from "./OnSaleRefirement";
import RatingRefinement from "./RatingRefinement";
import { reviewsEnv } from "../../../lib/resolve-reviews-field-env";

const ProductSpecification = () => {
return (
<>
{reviewsEnv.enable && (
<RatingRefinement attribute={reviewsEnv.avgRatingField} />
)}
<BrandRefinement attribute="ep_extensions_products_specifications.brand" />
<OnSaleRefinement attribute="ep_extensions_products_specifications.on-sale" />
<ColorRefinement attribute="ep_extensions_products_specifications.color" />
Expand Down
Loading