# Shopify Reviews Widget

React + Vite + TypeScript widgets for the public reviews API from `panel`. Widget interface text is available in
English, Ukrainian, and Russian. English remains the default unless a language option is explicitly configured.

## Local run

```bash
cd widgets
npm install
npm run dev
```

Open the dev dashboard to preview and test every widget from one page:

```text
http://localhost:5173/dev.html
```

The dashboard includes a mock API mode for product summary, store summary, store reviews, and product card ratings.

Open the widget with API parameters:

```text
http://localhost:5173/?shop=your-shop.myshopify.com&handle=product-handle
```

The panel app should be running on `http://localhost:3000`.

Dynamic HTML example:

```bash
npm run build
```

```text
http://localhost:5173/examples/dynamic-embed.html?shop=your-shop.myshopify.com&handle=product-handle
```

This page loads the built widget script from `dist/shopify-reviews-widget.js`; widget styles are injected into Shadow DOM.

## Embed

Product summary widget:

```html
<div
  data-shopify-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-product-handle="product-handle"
  data-language="en"
></div>
<script type="module" src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-widget.js"></script>
```

Product card rating widget:

```html
<div
  data-shopify-reviews-product-card-widget
  data-product-id="{{ product.id }}"
  data-hide-empty="true"
></div>

<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-product-card-widget.js"
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-language="en"
  data-body-font-family='Arial, "Segoe UI", sans-serif'
></script>
```

Store summary widget:

```html
<div
  data-shopify-reviews-store-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-language="en"
></div>
<script type="module" src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-store-widget.js"></script>
```

Store reviews list widget:

```html
<div
  data-shopify-reviews-store-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-page-size="6"
  data-hide-ratings="1,2"
  data-prioritize-country="UA"
  data-source-label="Etsy"
  data-source-url="https://www.etsy.com/shop/example#reviews"
  data-language="en"
></div>
<script type="module" src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-store-reviews-widget.js"></script>
```

Product reviews widget:

```html
<div
  data-shopify-reviews-product-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-product-id="{{ product.id }}"
  data-page-size="6"
  data-hide-ratings="1,2"
  data-prioritize-country="UA"
  data-source-label="Etsy"
  data-source-url="https://www.etsy.com/shop/example#reviews"
  data-language="en"
></div>
<script type="module" src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-product-reviews-widget.js"></script>
```

## Language

Use `data-language` on a widget mount to select a fixed language or to opt in to Shopify storefront language
detection.

| Value | Behavior |
| --- | --- |
| `en` | English |
| `uk` | Ukrainian |
| `ru` | Russian |
| `shopify` | Reads the current storefront `<html lang>` when the widget mounts |

If `data-language` is absent, the widget uses English. Shopify detection is never enabled implicitly; it only runs
when the value is explicitly set to `shopify`. Shopify locale tags such as `uk-UA`, `ru-RU`, and `en-US` resolve to
their supported base language. A missing, unknown, or unsupported language falls back to English.

To follow the language currently selected in Shopify, use:

```liquid
<div
  data-shopify-reviews-store-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="{{ shop.permanent_domain }}"
  data-language="shopify"
></div>
```

Shopify storefront language selectors normally load the selected locale page, so the widget resolves the new
`<html lang>` value when it mounts on that page. A fixed `uk` or `ru` value does not follow Shopify and always keeps
the chosen widget language.

The same setting can be passed through the page query string for previews and dynamic embeds:

```text
?language=en
?language=uk
?language=ru
?language=shopify
```

When both are present, `data-language` on the widget mount takes precedence over the `language` query parameter.
For the product card widget, put `data-language` on the single script tag to configure every card, or on an
individual card mount to override it. Its full precedence is card mount, page query parameter, then script tag:

```liquid
<div
  data-shopify-reviews-product-card-widget
  data-product-id="{{ product.id }}"
  data-hide-empty="true"
></div>

<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-product-card-widget.js"
  data-api-url="https://panel.example.com"
  data-shop="{{ shop.permanent_domain }}"
  data-language="shopify"
></script>
```

Localization covers widget headings, states, controls, counts, number formatting, and accessibility labels. Review
titles and bodies are displayed as returned by the panel API.

## Review sources and translations

Store and product review cards can show a linked source marker. Set `data-source-label` and `data-source-url` on
the widget mount to use one source for every review rendered by that widget:

```html
<div
  data-shopify-reviews-store-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-source-label="Etsy"
  data-source-url="https://www.etsy.com/shop/example#reviews"
></div>
```

The same values are available as `sourceLabel` and `sourceUrl` query parameters in standalone previews and
dynamic embeds. Widget-level values override the source label and URL returned for individual reviews. If no
override is set, imported `source` and `source_url` values are preserved per review, so a single review list can
link Etsy, Trustpilot, Google, or other sources independently. Source URLs must use HTTP or HTTPS. Without an
override, the marker is shown only when a review has a safe source URL, so internal values such as `Manual` are not
exposed. Setting only `data-source-label` intentionally shows a non-linked marker.

When the API returns a non-empty `originalBody` that differs from the displayed review body, the card shows a
localized translated-review marker. Hovering or focusing the marker reveals the original text; tapping or clicking
it keeps the original available on touch and keyboard-controlled storefronts. Reviews without a distinct original
body do not show the marker.

## Country priority

Store and product review lists can place reviews from one country before all other reviews. Set an ISO 3166-1
alpha-2 country code on the widget mount:

```html
<div
  data-shopify-reviews-product-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-product-id="{{ product.id }}"
  data-prioritize-country="UA"
></div>
```

The option is also available as the `prioritizeCountry=UA` query parameter for standalone previews. The code is
case-insensitive. Matching reviews are ordered first across the complete paginated result; their existing priority
and date order is preserved, as is the order of all remaining reviews. Omitting the option keeps the current order.

Typography can be configured on any widget:

```html
<div
  data-shopify-reviews-store-reviews-widget
  data-api-url="https://panel.example.com"
  data-shop="your-shop.myshopify.com"
  data-font-family='Inter, "Segoe UI", sans-serif'
  data-heading-font-family='Georgia, "Times New Roman", serif'
  data-body-font-family='Arial, sans-serif'
></div>
```

The same values can be passed as query params: `fontFamily`, `headingFontFamily`, and `bodyFontFamily`.
For CSS-only customization, set `--shopify-reviews-font-family`, `--shopify-reviews-heading-font-family`, or `--shopify-reviews-body-font-family` on the widget host element.
The product card rating widget has no separate heading typography; `data-body-font-family` controls all text in
that widget, including the review count and popover rows.

Supported filters:

- `data-product-id` for the product reviews and product card widgets; this is treated as the Shopify product ID
- `data-shop`
- `data-product-handle`
- `data-shopify-product-id`
- `data-panel-product-id`
- `data-hide-empty="true"` for the product card widget if zero-review products should render nothing; this is the default
- `data-hide-empty="false"` or `data-show-empty="true"` for the product card widget if zero-review products should still render
- `data-hide-popover="true"` for the product card widget to hide both the arrow and the rating breakdown popover
- `data-page-size` for the store reviews list and product reviews widgets
- `data-hide-ratings` for the store reviews list and product reviews widgets, for example `1,2`
- `data-prioritize-country` for the store reviews list and product reviews widgets, for example `UA`
- `data-source-label` to override the source marker for store and product review cards
- `data-source-url` to override the HTTP(S) link used by that source marker
- `data-language="en|uk|ru|shopify"`; omit it for English, or choose `shopify` to opt in to storefront detection
- `data-font-family`
- `data-heading-font-family`
- `data-body-font-family`

For product grids, include the product card widget script once per page. It batches all card widgets with
`data-shopify-reviews-product-card-widget` into requests to `/api/widgets/product-card-summaries`, then renders
each card from the shared response. Call `window.ShopifyReviewsProductCardWidget.refresh()` after dynamically
adding more product cards.

## Optimized Product Card Install

Use this setup for collection grids, search results, recommendations, and any page with many product cards.

### 1. Add the mount point inside the product card snippet

Put the mount point where the rating should appear, for example in `snippets/card-product.liquid`,
`snippets/product-card.liquid`, or the theme's equivalent card snippet.

```liquid
{% assign reviews_product = card_product | default: product %}

{% if reviews_product %}
  <div
    data-shopify-reviews-product-card-widget
    data-product-id="{{ reviews_product.id }}"
    data-hide-empty="true"
    data-hide-popover="true"
  ></div>
{% endif %}
```

`data-product-id` should be the Shopify numeric product ID. The widget also supports
`data-shopify-product-id`, `data-panel-product-id`, and `data-product-handle`, but the numeric Shopify ID is
the preferred option for product grids.

### 2. Include the script once per page

Do not put the script tag inside the product card loop. Add it once in `layout/theme.liquid`, ideally near
the end of `<body>`, or in a section that is rendered only once on pages with product grids.

```liquid
<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/agile-alpaca-shopify-reviews-widget@0.0.17/dist/shopify-reviews-product-card-widget.js"
  data-api-url="https://panel.example.com"
  data-shop="{{ shop.permanent_domain }}"
  data-language="shopify"
  data-body-font-family='Arial, "Segoe UI", sans-serif'
></script>
```

`type="module"` is deferred by default, so it does not block initial HTML parsing. If the script is included
before the product cards exist, it waits for `DOMContentLoaded`; if it is included after the grid, it starts
immediately.

### 3. Keep empty products invisible

`data-hide-empty="true"` is the default and is recommended for product grids. Products with no published
reviews render nothing, so the card layout stays clean and the page avoids unnecessary zero-state UI.

Use this only when you intentionally want to show an empty rating state:

```liquid
<div
  data-shopify-reviews-product-card-widget
  data-product-id="{{ reviews_product.id }}"
  data-hide-empty="false"
></div>
```

### 4. Refresh after AJAX-loaded cards

If the theme replaces or appends product cards after filtering, pagination, infinite scroll, quick search, or
recommendations loading, call `refresh()` after the new HTML is inserted.

```html
<script>
  document.addEventListener("products:loaded", function () {
    window.ShopifyReviewsProductCardWidget?.refresh();
  });
</script>
```

Use the real event or callback from the theme/app that updates the grid. The widget marks initialized nodes,
so calling `refresh()` again will only mount newly added cards.

If the theme has no useful callback, use a scoped and debounced observer on the product grid only:

```html
<script>
  var grid = document.querySelector("[data-product-grid]");
  var timer;

  if (grid) {
    new MutationObserver(function () {
      window.clearTimeout(timer);
      timer = window.setTimeout(function () {
        window.ShopifyReviewsProductCardWidget?.refresh();
      }, 80);
    }).observe(grid, { childList: true, subtree: true });
  }
</script>
```

### 5. Performance checklist

- Include one product card widget script per page, never one script per product.
- Prefer `data-product-id="{{ reviews_product.id }}"` over handle lookup for grids, using the product object from the card snippet.
- Keep `data-hide-empty="true"` unless the design requires zero-review UI.
- Let the browser honor the public widget API cache headers; do not use `cache: "no-store"` for these GETs.
  The API varies its CDN cache by the normalized shop and complete product target query.
- Add `preconnect` hints if the widget CDN or panel API is on a different origin and product cards are
  visible above the fold.
- Add a server-side cache only if its key includes the normalized shop and full normalized product target list.

Optional preconnect example:

```liquid
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://panel.example.com" crossorigin>
```

Store summary local example:

```text
http://localhost:5173/examples/dynamic-store-embed.html?shop=your-shop.myshopify.com&language=uk
```

Store reviews list local example:

```text
http://localhost:5173/examples/dynamic-store-reviews-embed.html?shop=your-shop.myshopify.com&take=6&hideRatings=1,2&prioritizeCountry=UA&language=ru
```

Product reviews local example:

```text
http://localhost:5173/product-reviews.html?shop=your-shop.myshopify.com&productId=1234567890&take=6&prioritizeCountry=UA&language=uk
```

Product card local example:

```text
http://localhost:5173/examples/dynamic-product-card-embed.html?shop=your-shop.myshopify.com&productIds=1234567890,9876543210&language=shopify
```
