judgeme-hydrogen-fixed 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.1] - 2026-01-06
9
+
10
+ ### Changed
11
+ - Updated documentation and GitHub repository links
12
+
13
+ ## [1.0.0] - 2026-01-06
14
+
15
+ ### Added
16
+ - Initial release
17
+ - `useJudgeme` hook with proper dependency arrays (fixing the infinite render loop bug from the original package)
18
+ - All Judge.me widget components:
19
+ - `JudgemePreviewBadge` - Star rating badge for product cards
20
+ - `JudgemeReviewWidget` - Full review widget for product pages
21
+ - `JudgemeCarousel` - Review carousel widget
22
+ - `JudgemeReviewsTab` - Reviews tab widget
23
+ - `JudgemeAllReviewsRating` - Overall rating display
24
+ - `JudgemeVerifiedBadge` - Verified badge widget
25
+ - `JudgemeAllReviewsCount` - Total reviews count
26
+ - `JudgemeMedals` - Medals/badges widget
27
+ - Full TypeScript support
28
+ - Proper cleanup of timeouts to prevent memory leaks
29
+
30
+ ### Fixed
31
+
32
+ - Fixed infinite render loop caused by `useEffect` with no dependency array in original `@judgeme/shopify-hydrogen` package
33
+ - Fixed infinite refresh loops caused by `installed.js` when deployed to Shopify Oxygen
34
+ - Widgets now initialize by calling `jdgm_preloader()` directly instead of relying on problematic bootstrap script
package/README.md CHANGED
@@ -1,21 +1,15 @@
1
1
  # judgeme-hydrogen-fixed
2
2
 
3
- A fixed version of the official `@judgeme/shopify-hydrogen` package for integrating Judge.me reviews into Shopify Hydrogen stores.
3
+ A fixed version of `@judgeme/shopify-hydrogen` for Shopify Hydrogen/Oxygen that eliminates infinite refresh loops and improper React hook usage.
4
4
 
5
- ## Why this package?
5
+ ## Why This Package?
6
6
 
7
- The official `@judgeme/shopify-hydrogen` package has a bug in its `useJudgeme` hook - it contains a `useEffect` with **no dependency array**, which causes it to run on every render. This can lead to:
7
+ The official `@judgeme/shopify-hydrogen` package has critical bugs that make it unusable in production:
8
8
 
9
- - Infinite refresh loops
10
- - Performance issues
11
- - Memory leaks from uncleaned timeouts
9
+ 1. **Infinite render loop** - `useEffect` with no dependency array runs on every render
10
+ 2. **Refresh loop on Oxygen** - `installed.js` causes page refreshes in production deployments
12
11
 
13
- This package provides a properly implemented version that:
14
-
15
- - ✅ Loads Judge.me scripts only once on mount
16
- - ✅ Re-renders widgets only on route changes (not every render)
17
- - ✅ Properly cleans up timeouts to prevent memory leaks
18
- - ✅ Includes proper TypeScript types
12
+ This package fixes both issues while maintaining full compatibility with Judge.me widgets.
19
13
 
20
14
  ## Installation
21
15
 
@@ -25,130 +19,138 @@ npm install judgeme-hydrogen-fixed
25
19
 
26
20
  ## Usage
27
21
 
28
- ### 1. Initialize Judge.me in your root component
22
+ ### 1. Add the Hook to Your Root Component
23
+
24
+ Add `useJudgeme` to your root layout or App component:
29
25
 
30
26
  ```tsx
31
27
  // app/root.tsx
32
- import { useJudgeme } from "judgeme-hydrogen-fixed";
28
+ import { useJudgeme } from 'judgeme-hydrogen-fixed';
33
29
 
34
30
  export default function App() {
35
31
  useJudgeme({
36
- shopDomain: "your-store.myshopify.com",
37
- publicToken: "your-judge-me-public-token",
38
- cdnHost: "https://cdn.judge.me",
32
+ shopDomain: 'your-store.myshopify.com',
33
+ publicToken: 'your-judge-me-public-token',
34
+ cdnHost: 'https://cdn.judge.me',
39
35
  delay: 500, // optional, defaults to 500ms
40
36
  });
41
37
 
42
- return <Outlet />;
38
+ return (
39
+ <html>
40
+ {/* ... */}
41
+ </html>
42
+ );
43
43
  }
44
44
  ```
45
45
 
46
- ### 2. Add widgets to your product pages
46
+ ### 2. Add Widget Components
47
47
 
48
- ```tsx
49
- // app/routes/products.$handle.tsx
50
- import {
51
- JudgemePreviewBadge,
52
- JudgemeReviewWidget,
53
- } from "judgeme-hydrogen-fixed";
48
+ #### Preview Badge (Star Rating)
54
49
 
55
- function ProductPage({ product }) {
56
- const productId = product.id.replace("gid://shopify/Product/", "");
50
+ Display star ratings on product cards or near product titles:
51
+
52
+ ```tsx
53
+ import { JudgemePreviewBadge } from 'judgeme-hydrogen-fixed';
57
54
 
55
+ function ProductCard({ product }) {
58
56
  return (
59
57
  <div>
60
- <h1>{product.title}</h1>
58
+ <h2>{product.title}</h2>
59
+ <JudgemePreviewBadge
60
+ id={product.id}
61
+ productTitle={product.title}
62
+ productHandle={product.handle}
63
+ />
64
+ </div>
65
+ );
66
+ }
67
+ ```
68
+
69
+ #### Review Widget (Full Reviews Section)
61
70
 
62
- {/* Star rating badge - place near title */}
63
- <JudgemePreviewBadge id={productId} template="product" />
71
+ Display the complete reviews section on product pages:
64
72
 
65
- {/* Product details... */}
73
+ ```tsx
74
+ import { JudgemeReviewWidget } from 'judgeme-hydrogen-fixed';
66
75
 
67
- {/* Full review widget - place at bottom */}
68
- <JudgemeReviewWidget id={productId} />
76
+ function ProductPage({ product }) {
77
+ return (
78
+ <div>
79
+ {/* Product content */}
80
+
81
+ <JudgemeReviewWidget
82
+ id={product.id}
83
+ productTitle={product.title}
84
+ productHandle={product.handle}
85
+ productImageUrl={product.featuredImage?.url}
86
+ productDescription={product.description}
87
+ />
69
88
  </div>
70
89
  );
71
90
  }
72
91
  ```
73
92
 
74
- ## Available Components
75
-
76
- | Component | Description |
77
- | ------------------------- | ---------------------------------------- |
78
- | `JudgemePreviewBadge` | Star rating badge for product pages |
79
- | `JudgemeReviewWidget` | Full reviews list with write review form |
80
- | `JudgemeCarousel` | Carousel of recent reviews |
81
- | `JudgemeReviewsTab` | Reviews tab for tabbed layouts |
82
- | `JudgemeAllReviewsRating` | Overall store rating |
83
- | `JudgemeVerifiedBadge` | Verified store badge |
84
- | `JudgemeAllReviewsCount` | Total review count |
85
- | `JudgemeMedals` | Store medals/achievements |
86
-
87
- ## Configuration
88
-
89
- ### useJudgeme Options
90
-
91
- | Option | Type | Required | Default | Description |
92
- | ------------- | -------- | -------- | ------- | -------------------------------------------------- |
93
- | `shopDomain` | `string` | Yes | - | Your Shopify store domain |
94
- | `publicToken` | `string` | Yes | - | Your Judge.me public token |
95
- | `cdnHost` | `string` | Yes | - | Judge.me CDN host (usually `https://cdn.judge.me`) |
96
- | `delay` | `number` | No | `500` | Delay before re-rendering widgets on route change |
97
-
98
- ## CSP Configuration
99
-
100
- If you're using Content Security Policy, add these domains:
101
-
102
- ```typescript
103
- // entry.server.tsx
104
- const scriptSrc = [
105
- // ... other sources
106
- "https://cdn.judge.me",
107
- "https://cdnwidget.judge.me",
108
- "https://*.judge.me",
109
- ];
110
-
111
- const connectSrc = [
112
- // ... other sources
113
- "https://cdn.judge.me",
114
- "https://*.judge.me",
115
- ];
116
-
117
- const styleSrc = [
118
- // ... other sources
119
- "https://*.judge.me",
120
- ];
121
-
122
- const imgSrc = [
123
- // ... other sources
124
- "https://*.judge.me",
125
- ];
126
- ```
93
+ ### Available Components
127
94
 
128
- ## The Bug Fix Explained
95
+ | Component | Description |
96
+ |-----------|-------------|
97
+ | `JudgemePreviewBadge` | Star rating badge for product cards |
98
+ | `JudgemeReviewWidget` | Full review widget with reviews list and write review form |
99
+ | `JudgemeCarousel` | Review carousel widget |
100
+ | `JudgemeReviewsTab` | Reviews tab widget |
101
+ | `JudgemeAllReviewsRating` | Overall rating display across all products |
102
+ | `JudgemeVerifiedBadge` | Verified badge for social proof |
103
+ | `JudgemeAllReviewsCount` | Total reviews count across all products |
104
+ | `JudgemeMedals` | Store medals/badges widget |
129
105
 
130
- The original package has this code:
106
+ ## How It Works
131
107
 
132
- ```javascript
133
- // ❌ Original (buggy) - runs on EVERY render
134
- useEffect(() => {
135
- // Widget re-render logic...
136
- }); // No dependency array!
137
- ```
108
+ ### The Problem with installed.js
109
+
110
+ The official Judge.me package loads `installed.js` which performs automatic initialization. On Shopify Oxygen (and other edge deployments), this script causes infinite page refresh loops.
111
+
112
+ ### Our Solution
138
113
 
139
- This package fixes it:
114
+ 1. **No installed.js** - We don't load the problematic bootstrap script
115
+ 2. **Direct preloader call** - Widgets are initialized by calling `jdgm_preloader()` directly
116
+ 3. **Smart retry mechanism** - Waits for widget elements to appear in DOM before initializing
117
+ 4. **Proper React hooks** - Correct dependency arrays prevent infinite render loops
118
+ 5. **Route change handling** - Widgets re-initialize on client-side navigation
140
119
 
141
- ```javascript
142
- // ✅ Fixed - runs only on route changes
143
- useEffect(() => {
144
- // Widget re-render logic...
145
- }, [location.pathname, location.search, delay]);
120
+ ## Configuration Options
121
+
122
+ | Option | Type | Required | Default | Description |
123
+ |--------|------|----------|---------|-------------|
124
+ | `shopDomain` | `string` | Yes | - | Your Shopify store domain |
125
+ | `publicToken` | `string` | Yes | - | Your Judge.me public token |
126
+ | `cdnHost` | `string` | Yes | - | Judge.me CDN host (usually `https://cdn.judge.me`) |
127
+ | `delay` | `number` | No | `500` | Delay (ms) before re-rendering widgets on route change |
128
+
129
+ ## TypeScript Support
130
+
131
+ Full TypeScript support is included. All components and the hook are fully typed.
132
+
133
+ ```tsx
134
+ import type {
135
+ UseJudgemeConfig,
136
+ JudgemePreviewBadgeProps,
137
+ JudgemeReviewWidgetProps,
138
+ } from 'judgeme-hydrogen-fixed';
146
139
  ```
147
140
 
141
+ ## Finding Your Judge.me Credentials
142
+
143
+ 1. Log in to your [Judge.me dashboard](https://judge.me/dashboard)
144
+ 2. Go to **Settings** > **Technical**
145
+ 3. Find your **Public Token** and **Shop Domain**
146
+ 4. The CDN host is typically `https://cdn.judge.me`
147
+
148
148
  ## License
149
149
 
150
- MIT
150
+ MIT © Ben Goodman
151
151
 
152
- ## Contributing
152
+ ## Links
153
153
 
154
- Issues and PRs welcome! If Judge.me fixes the official package, this one will be deprecated.
154
+ - [GitHub Repository](https://github.com/ben-goodman-uk/judgeme-hydrogen-fixed)
155
+ - [Report Issues](https://github.com/ben-goodman-uk/judgeme-hydrogen-fixed/issues)
156
+ - [npm Package](https://www.npmjs.com/package/judgeme-hydrogen-fixed)
package/dist/index.d.mts CHANGED
@@ -6,13 +6,26 @@ declare global {
6
6
  SHOP_DOMAIN?: string;
7
7
  PLATFORM?: string;
8
8
  PUBLIC_TOKEN?: string;
9
- productData?: unknown;
9
+ productData?: {
10
+ id: string;
11
+ title: string;
12
+ handle: string;
13
+ description?: string;
14
+ image?: string;
15
+ };
10
16
  };
11
17
  jdgm_preloader?: () => void;
12
18
  jdgmCacheServer?: {
13
19
  reloadAll: () => void;
14
20
  };
15
21
  jdgm_rerender?: ReturnType<typeof setTimeout>;
22
+ JDGM_PRODUCT?: {
23
+ id: string;
24
+ title: string;
25
+ handle: string;
26
+ description?: string;
27
+ url?: string;
28
+ };
16
29
  }
17
30
  }
18
31
  interface UseJudgemeConfig {
@@ -44,12 +57,40 @@ interface JudgemePreviewBadgeProps {
44
57
  * @default 'product'
45
58
  */
46
59
  template?: 'product' | 'collection';
60
+ /**
61
+ * Product title - needed for the review modal
62
+ */
63
+ productTitle?: string;
64
+ /**
65
+ * Product handle/slug
66
+ */
67
+ productHandle?: string;
68
+ /**
69
+ * Product image URL
70
+ */
71
+ productImageUrl?: string;
47
72
  }
48
73
  interface JudgemeReviewWidgetProps {
49
74
  /**
50
75
  * Shopify product ID (numeric ID only, not the full GID)
51
76
  */
52
77
  id: string;
78
+ /**
79
+ * Product title - needed for the review modal
80
+ */
81
+ productTitle?: string;
82
+ /**
83
+ * Product handle/slug
84
+ */
85
+ productHandle?: string;
86
+ /**
87
+ * Product image URL
88
+ */
89
+ productImageUrl?: string;
90
+ /**
91
+ * Product description
92
+ */
93
+ productDescription?: string;
53
94
  }
54
95
  interface JudgemeCarouselProps {
55
96
  }
@@ -65,20 +106,25 @@ interface JudgemeMedalsProps {
65
106
  }
66
107
 
67
108
  /**
68
- * Fixed version of useJudgeme that properly handles React's render cycle.
109
+ * Fixed version of useJudgeme for Shopify Hydrogen/Oxygen.
110
+ *
111
+ * ## Problems with the official @judgeme/shopify-hydrogen package:
69
112
  *
70
- * The original @judgeme/shopify-hydrogen package has a useEffect with no
71
- * dependency array that runs on every render, causing infinite refresh loops.
113
+ * 1. **Infinite render loop**: useEffect with no dependency array runs on every render
114
+ * 2. **Refresh loop on Oxygen**: installed.js causes page refreshes in production
72
115
  *
73
- * This fixed version:
74
- * 1. Loads Judge.me scripts only once on mount
75
- * 2. Re-renders widgets only on route changes (not every render)
76
- * 3. Properly cleans up timeouts to prevent memory leaks
116
+ * ## How this version fixes it:
117
+ *
118
+ * 1. **No installed.js** - Does not load installed.js which causes refresh loops on Oxygen
119
+ * 2. **Direct preloader call** - Calls jdgm_preloader() directly to initialize widgets
120
+ * 3. **Proper dependencies** - Re-renders widgets only on actual route changes
121
+ * 4. **Global deduplication** - Prevents double-loading across component remounts
122
+ * 5. **Proper cleanup** - Cleans up timeouts to prevent memory leaks
77
123
  *
78
124
  * @example
79
125
  * ```tsx
80
126
  * // In your root.tsx or App component
81
- * import { useJudgeme } from '@judgeme/hydrogen-fixed';
127
+ * import { useJudgeme } from 'judgeme-hydrogen-fixed';
82
128
  *
83
129
  * function App() {
84
130
  * useJudgeme({
@@ -91,6 +137,12 @@ interface JudgemeMedalsProps {
91
137
  * return <Outlet />;
92
138
  * }
93
139
  * ```
140
+ *
141
+ * @param config - Configuration options
142
+ * @param config.shopDomain - Your Shopify store domain (e.g., 'your-store.myshopify.com')
143
+ * @param config.publicToken - Your Judge.me public token
144
+ * @param config.cdnHost - Judge.me CDN host (usually 'https://cdn.judge.me')
145
+ * @param config.delay - Delay before re-rendering widgets on route change (default: 500ms)
94
146
  */
95
147
  declare function useJudgeme({ shopDomain, publicToken, cdnHost, delay, }: UseJudgemeConfig): void;
96
148
 
@@ -98,12 +150,12 @@ declare function useJudgeme({ shopDomain, publicToken, cdnHost, delay, }: UseJud
98
150
  * Preview badge showing star rating for a product
99
151
  * Place this near the product title on product pages
100
152
  */
101
- declare function JudgemePreviewBadge({ id, template, }: JudgemePreviewBadgeProps): React.ReactElement;
153
+ declare function JudgemePreviewBadge({ id, template, productTitle, productHandle, productImageUrl, }: JudgemePreviewBadgeProps): React.ReactElement;
102
154
  /**
103
155
  * Full review widget with reviews list and write review form
104
156
  * Place this at the bottom of product pages
105
157
  */
106
- declare function JudgemeReviewWidget({ id, }: JudgemeReviewWidgetProps): React.ReactElement;
158
+ declare function JudgemeReviewWidget({ id, productTitle, productHandle, productImageUrl, productDescription, }: JudgemeReviewWidgetProps): React.ReactElement;
107
159
  /**
108
160
  * Carousel showing recent reviews across all products
109
161
  */
package/dist/index.d.ts CHANGED
@@ -6,13 +6,26 @@ declare global {
6
6
  SHOP_DOMAIN?: string;
7
7
  PLATFORM?: string;
8
8
  PUBLIC_TOKEN?: string;
9
- productData?: unknown;
9
+ productData?: {
10
+ id: string;
11
+ title: string;
12
+ handle: string;
13
+ description?: string;
14
+ image?: string;
15
+ };
10
16
  };
11
17
  jdgm_preloader?: () => void;
12
18
  jdgmCacheServer?: {
13
19
  reloadAll: () => void;
14
20
  };
15
21
  jdgm_rerender?: ReturnType<typeof setTimeout>;
22
+ JDGM_PRODUCT?: {
23
+ id: string;
24
+ title: string;
25
+ handle: string;
26
+ description?: string;
27
+ url?: string;
28
+ };
16
29
  }
17
30
  }
18
31
  interface UseJudgemeConfig {
@@ -44,12 +57,40 @@ interface JudgemePreviewBadgeProps {
44
57
  * @default 'product'
45
58
  */
46
59
  template?: 'product' | 'collection';
60
+ /**
61
+ * Product title - needed for the review modal
62
+ */
63
+ productTitle?: string;
64
+ /**
65
+ * Product handle/slug
66
+ */
67
+ productHandle?: string;
68
+ /**
69
+ * Product image URL
70
+ */
71
+ productImageUrl?: string;
47
72
  }
48
73
  interface JudgemeReviewWidgetProps {
49
74
  /**
50
75
  * Shopify product ID (numeric ID only, not the full GID)
51
76
  */
52
77
  id: string;
78
+ /**
79
+ * Product title - needed for the review modal
80
+ */
81
+ productTitle?: string;
82
+ /**
83
+ * Product handle/slug
84
+ */
85
+ productHandle?: string;
86
+ /**
87
+ * Product image URL
88
+ */
89
+ productImageUrl?: string;
90
+ /**
91
+ * Product description
92
+ */
93
+ productDescription?: string;
53
94
  }
54
95
  interface JudgemeCarouselProps {
55
96
  }
@@ -65,20 +106,25 @@ interface JudgemeMedalsProps {
65
106
  }
66
107
 
67
108
  /**
68
- * Fixed version of useJudgeme that properly handles React's render cycle.
109
+ * Fixed version of useJudgeme for Shopify Hydrogen/Oxygen.
110
+ *
111
+ * ## Problems with the official @judgeme/shopify-hydrogen package:
69
112
  *
70
- * The original @judgeme/shopify-hydrogen package has a useEffect with no
71
- * dependency array that runs on every render, causing infinite refresh loops.
113
+ * 1. **Infinite render loop**: useEffect with no dependency array runs on every render
114
+ * 2. **Refresh loop on Oxygen**: installed.js causes page refreshes in production
72
115
  *
73
- * This fixed version:
74
- * 1. Loads Judge.me scripts only once on mount
75
- * 2. Re-renders widgets only on route changes (not every render)
76
- * 3. Properly cleans up timeouts to prevent memory leaks
116
+ * ## How this version fixes it:
117
+ *
118
+ * 1. **No installed.js** - Does not load installed.js which causes refresh loops on Oxygen
119
+ * 2. **Direct preloader call** - Calls jdgm_preloader() directly to initialize widgets
120
+ * 3. **Proper dependencies** - Re-renders widgets only on actual route changes
121
+ * 4. **Global deduplication** - Prevents double-loading across component remounts
122
+ * 5. **Proper cleanup** - Cleans up timeouts to prevent memory leaks
77
123
  *
78
124
  * @example
79
125
  * ```tsx
80
126
  * // In your root.tsx or App component
81
- * import { useJudgeme } from '@judgeme/hydrogen-fixed';
127
+ * import { useJudgeme } from 'judgeme-hydrogen-fixed';
82
128
  *
83
129
  * function App() {
84
130
  * useJudgeme({
@@ -91,6 +137,12 @@ interface JudgemeMedalsProps {
91
137
  * return <Outlet />;
92
138
  * }
93
139
  * ```
140
+ *
141
+ * @param config - Configuration options
142
+ * @param config.shopDomain - Your Shopify store domain (e.g., 'your-store.myshopify.com')
143
+ * @param config.publicToken - Your Judge.me public token
144
+ * @param config.cdnHost - Judge.me CDN host (usually 'https://cdn.judge.me')
145
+ * @param config.delay - Delay before re-rendering widgets on route change (default: 500ms)
94
146
  */
95
147
  declare function useJudgeme({ shopDomain, publicToken, cdnHost, delay, }: UseJudgemeConfig): void;
96
148
 
@@ -98,12 +150,12 @@ declare function useJudgeme({ shopDomain, publicToken, cdnHost, delay, }: UseJud
98
150
  * Preview badge showing star rating for a product
99
151
  * Place this near the product title on product pages
100
152
  */
101
- declare function JudgemePreviewBadge({ id, template, }: JudgemePreviewBadgeProps): React.ReactElement;
153
+ declare function JudgemePreviewBadge({ id, template, productTitle, productHandle, productImageUrl, }: JudgemePreviewBadgeProps): React.ReactElement;
102
154
  /**
103
155
  * Full review widget with reviews list and write review form
104
156
  * Place this at the bottom of product pages
105
157
  */
106
- declare function JudgemeReviewWidget({ id, }: JudgemeReviewWidgetProps): React.ReactElement;
158
+ declare function JudgemeReviewWidget({ id, productTitle, productHandle, productImageUrl, productDescription, }: JudgemeReviewWidgetProps): React.ReactElement;
107
159
  /**
108
160
  * Carousel showing recent reviews across all products
109
161
  */
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ module.exports = __toCommonJS(index_exports);
35
35
  // src/useJudgeme.ts
36
36
  var import_react = require("react");
37
37
  var import_react2 = require("@remix-run/react");
38
+ var globalScriptsLoaded = false;
38
39
  function useJudgeme({
39
40
  shopDomain,
40
41
  publicToken,
@@ -42,75 +43,92 @@ function useJudgeme({
42
43
  delay = 500
43
44
  }) {
44
45
  const location = (0, import_react2.useLocation)();
45
- const isLoadedRef = (0, import_react.useRef)(false);
46
+ const lastPathnameRef = (0, import_react.useRef)("");
46
47
  const rerenderTimeoutRef = (0, import_react.useRef)(null);
48
+ const isInitializedRef = (0, import_react.useRef)(false);
49
+ const refreshWidgets = (0, import_react.useCallback)(() => {
50
+ if (typeof window === "undefined") return;
51
+ const attemptRefresh = (retriesLeft) => {
52
+ const widgets = document.querySelectorAll(".jdgm-widget");
53
+ if (widgets.length === 0 && retriesLeft > 0) {
54
+ setTimeout(() => attemptRefresh(retriesLeft - 1), 200);
55
+ return;
56
+ }
57
+ try {
58
+ if (typeof window.jdgm_preloader === "function" && !window.jdgmCacheServer) {
59
+ window.jdgm_preloader();
60
+ } else if (window.jdgmCacheServer) {
61
+ window.jdgmCacheServer.reloadAll();
62
+ }
63
+ } catch (e) {
64
+ console.warn("Judge.me: Error refreshing widgets", e);
65
+ }
66
+ };
67
+ attemptRefresh(10);
68
+ }, []);
47
69
  (0, import_react.useEffect)(() => {
48
- if (isLoadedRef.current) return;
70
+ if (typeof window === "undefined") return;
71
+ if (globalScriptsLoaded || isInitializedRef.current) return;
49
72
  if (!shopDomain || !publicToken || !cdnHost) {
50
73
  console.warn(
51
74
  "Judge.me: Missing config values for store domain, store public token, or cdn host"
52
75
  );
53
76
  return;
54
77
  }
55
- const shopCredentials = `
56
- if (typeof window.jdgm === 'undefined') {
57
- window.jdgm = {};
58
- window.jdgm.SHOP_DOMAIN = '${shopDomain}';
59
- window.jdgm.PLATFORM = 'shopify';
60
- window.jdgm.PUBLIC_TOKEN = '${publicToken}';
61
- }
62
- `;
78
+ globalScriptsLoaded = true;
79
+ isInitializedRef.current = true;
80
+ window.jdgm = window.jdgm || {};
81
+ window.jdgm.SHOP_DOMAIN = shopDomain;
82
+ window.jdgm.PLATFORM = "shopify";
83
+ window.jdgm.PUBLIC_TOKEN = publicToken;
63
84
  fetch(`${cdnHost}/widget_preloader.js`).then((res) => {
64
85
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
65
86
  return res.text();
66
87
  }).then((text) => {
67
- const preloaderFunction = `function jdgm_preloader(){${text}}`;
68
- const shopCredentialsScript = document.createElement("script");
69
88
  const preloaderScript = document.createElement("script");
70
- const installedScript = document.createElement("script");
71
- shopCredentialsScript.innerText = shopCredentials;
72
- preloaderScript.innerText = preloaderFunction;
73
- installedScript.src = `${cdnHost}/assets/installed.js`;
74
- document.head.append(
75
- shopCredentialsScript,
76
- preloaderScript,
77
- installedScript
78
- );
79
- isLoadedRef.current = true;
80
- console.log("Judge.me scripts loaded");
89
+ preloaderScript.innerText = `function jdgm_preloader(){${text}}`;
90
+ document.head.appendChild(preloaderScript);
91
+ console.log("Judge.me preloader script loaded, initializing widgets...");
92
+ refreshWidgets();
81
93
  }).catch((error) => {
82
94
  console.error("Judge.me: Failed to load scripts", error);
95
+ globalScriptsLoaded = false;
96
+ isInitializedRef.current = false;
83
97
  });
84
98
  return () => {
85
99
  if (rerenderTimeoutRef.current) {
86
100
  clearTimeout(rerenderTimeoutRef.current);
87
101
  }
88
102
  };
89
- }, [shopDomain, publicToken, cdnHost]);
103
+ }, [shopDomain, publicToken, cdnHost, refreshWidgets]);
90
104
  (0, import_react.useEffect)(() => {
105
+ if (typeof window === "undefined") return;
106
+ const normalizedPathname = location.pathname.replace(/\/$/, "") || "/";
107
+ if (lastPathnameRef.current === normalizedPathname) {
108
+ return;
109
+ }
110
+ lastPathnameRef.current = normalizedPathname;
91
111
  if (rerenderTimeoutRef.current) {
92
112
  clearTimeout(rerenderTimeoutRef.current);
93
113
  }
94
- rerenderTimeoutRef.current = setTimeout(() => {
95
- if (window.jdgm_preloader && !window.jdgmCacheServer) {
96
- window.jdgm_preloader();
97
- } else if (window.jdgmCacheServer) {
98
- window.jdgmCacheServer.reloadAll();
99
- }
100
- }, delay);
114
+ rerenderTimeoutRef.current = setTimeout(refreshWidgets, delay);
101
115
  return () => {
102
116
  if (rerenderTimeoutRef.current) {
103
117
  clearTimeout(rerenderTimeoutRef.current);
104
118
  }
105
119
  };
106
- }, [location.pathname, location.search, delay]);
120
+ }, [location.pathname, delay, refreshWidgets]);
107
121
  }
108
122
 
109
123
  // src/components.tsx
124
+ var import_react3 = require("react");
110
125
  var import_jsx_runtime = require("react/jsx-runtime");
111
126
  function JudgemePreviewBadge({
112
127
  id,
113
- template = "product"
128
+ template = "product",
129
+ productTitle,
130
+ productHandle,
131
+ productImageUrl
114
132
  }) {
115
133
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
116
134
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -119,15 +137,61 @@ function JudgemePreviewBadge({
119
137
  className: "jdgm-widget jdgm-preview-badge",
120
138
  "data-id": shopifyId,
121
139
  "data-template": template,
122
- "data-auto-install": "false"
140
+ "data-auto-install": "false",
141
+ ...productTitle && { "data-product-title": productTitle },
142
+ ...productHandle && { "data-product-handle": productHandle },
143
+ ...productImageUrl && { "data-product-url": productImageUrl }
123
144
  }
124
145
  );
125
146
  }
126
147
  function JudgemeReviewWidget({
127
- id
148
+ id,
149
+ productTitle,
150
+ productHandle,
151
+ productImageUrl,
152
+ productDescription
128
153
  }) {
129
154
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
130
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "jdgm-widget jdgm-review-widget", "data-id": shopifyId });
155
+ (0, import_react3.useEffect)(() => {
156
+ if (typeof window !== "undefined" && productTitle) {
157
+ if (!window.jdgm) {
158
+ window.jdgm = {};
159
+ }
160
+ window.jdgm.productData = {
161
+ id: shopifyId,
162
+ title: productTitle,
163
+ handle: productHandle || "",
164
+ description: productDescription || "",
165
+ image: productImageUrl || ""
166
+ };
167
+ window.JDGM_PRODUCT = {
168
+ id: shopifyId,
169
+ title: productTitle,
170
+ handle: productHandle || "",
171
+ description: productDescription || "",
172
+ url: productImageUrl || ""
173
+ };
174
+ }
175
+ }, [
176
+ shopifyId,
177
+ productTitle,
178
+ productHandle,
179
+ productImageUrl,
180
+ productDescription
181
+ ]);
182
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
183
+ "div",
184
+ {
185
+ className: "jdgm-widget jdgm-review-widget",
186
+ "data-id": shopifyId,
187
+ ...productTitle && { "data-product-title": productTitle },
188
+ ...productHandle && { "data-product-handle": productHandle },
189
+ ...productImageUrl && { "data-product-url": productImageUrl },
190
+ ...productDescription && {
191
+ "data-product-description": productDescription
192
+ }
193
+ }
194
+ );
131
195
  }
132
196
  function JudgemeCarousel() {
133
197
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "jdgm-widget jdgm-carousel-widget" });
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/useJudgeme.ts
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect, useRef, useCallback } from "react";
3
3
  import { useLocation } from "@remix-run/react";
4
+ var globalScriptsLoaded = false;
4
5
  function useJudgeme({
5
6
  shopDomain,
6
7
  publicToken,
@@ -8,75 +9,92 @@ function useJudgeme({
8
9
  delay = 500
9
10
  }) {
10
11
  const location = useLocation();
11
- const isLoadedRef = useRef(false);
12
+ const lastPathnameRef = useRef("");
12
13
  const rerenderTimeoutRef = useRef(null);
14
+ const isInitializedRef = useRef(false);
15
+ const refreshWidgets = useCallback(() => {
16
+ if (typeof window === "undefined") return;
17
+ const attemptRefresh = (retriesLeft) => {
18
+ const widgets = document.querySelectorAll(".jdgm-widget");
19
+ if (widgets.length === 0 && retriesLeft > 0) {
20
+ setTimeout(() => attemptRefresh(retriesLeft - 1), 200);
21
+ return;
22
+ }
23
+ try {
24
+ if (typeof window.jdgm_preloader === "function" && !window.jdgmCacheServer) {
25
+ window.jdgm_preloader();
26
+ } else if (window.jdgmCacheServer) {
27
+ window.jdgmCacheServer.reloadAll();
28
+ }
29
+ } catch (e) {
30
+ console.warn("Judge.me: Error refreshing widgets", e);
31
+ }
32
+ };
33
+ attemptRefresh(10);
34
+ }, []);
13
35
  useEffect(() => {
14
- if (isLoadedRef.current) return;
36
+ if (typeof window === "undefined") return;
37
+ if (globalScriptsLoaded || isInitializedRef.current) return;
15
38
  if (!shopDomain || !publicToken || !cdnHost) {
16
39
  console.warn(
17
40
  "Judge.me: Missing config values for store domain, store public token, or cdn host"
18
41
  );
19
42
  return;
20
43
  }
21
- const shopCredentials = `
22
- if (typeof window.jdgm === 'undefined') {
23
- window.jdgm = {};
24
- window.jdgm.SHOP_DOMAIN = '${shopDomain}';
25
- window.jdgm.PLATFORM = 'shopify';
26
- window.jdgm.PUBLIC_TOKEN = '${publicToken}';
27
- }
28
- `;
44
+ globalScriptsLoaded = true;
45
+ isInitializedRef.current = true;
46
+ window.jdgm = window.jdgm || {};
47
+ window.jdgm.SHOP_DOMAIN = shopDomain;
48
+ window.jdgm.PLATFORM = "shopify";
49
+ window.jdgm.PUBLIC_TOKEN = publicToken;
29
50
  fetch(`${cdnHost}/widget_preloader.js`).then((res) => {
30
51
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
31
52
  return res.text();
32
53
  }).then((text) => {
33
- const preloaderFunction = `function jdgm_preloader(){${text}}`;
34
- const shopCredentialsScript = document.createElement("script");
35
54
  const preloaderScript = document.createElement("script");
36
- const installedScript = document.createElement("script");
37
- shopCredentialsScript.innerText = shopCredentials;
38
- preloaderScript.innerText = preloaderFunction;
39
- installedScript.src = `${cdnHost}/assets/installed.js`;
40
- document.head.append(
41
- shopCredentialsScript,
42
- preloaderScript,
43
- installedScript
44
- );
45
- isLoadedRef.current = true;
46
- console.log("Judge.me scripts loaded");
55
+ preloaderScript.innerText = `function jdgm_preloader(){${text}}`;
56
+ document.head.appendChild(preloaderScript);
57
+ console.log("Judge.me preloader script loaded, initializing widgets...");
58
+ refreshWidgets();
47
59
  }).catch((error) => {
48
60
  console.error("Judge.me: Failed to load scripts", error);
61
+ globalScriptsLoaded = false;
62
+ isInitializedRef.current = false;
49
63
  });
50
64
  return () => {
51
65
  if (rerenderTimeoutRef.current) {
52
66
  clearTimeout(rerenderTimeoutRef.current);
53
67
  }
54
68
  };
55
- }, [shopDomain, publicToken, cdnHost]);
69
+ }, [shopDomain, publicToken, cdnHost, refreshWidgets]);
56
70
  useEffect(() => {
71
+ if (typeof window === "undefined") return;
72
+ const normalizedPathname = location.pathname.replace(/\/$/, "") || "/";
73
+ if (lastPathnameRef.current === normalizedPathname) {
74
+ return;
75
+ }
76
+ lastPathnameRef.current = normalizedPathname;
57
77
  if (rerenderTimeoutRef.current) {
58
78
  clearTimeout(rerenderTimeoutRef.current);
59
79
  }
60
- rerenderTimeoutRef.current = setTimeout(() => {
61
- if (window.jdgm_preloader && !window.jdgmCacheServer) {
62
- window.jdgm_preloader();
63
- } else if (window.jdgmCacheServer) {
64
- window.jdgmCacheServer.reloadAll();
65
- }
66
- }, delay);
80
+ rerenderTimeoutRef.current = setTimeout(refreshWidgets, delay);
67
81
  return () => {
68
82
  if (rerenderTimeoutRef.current) {
69
83
  clearTimeout(rerenderTimeoutRef.current);
70
84
  }
71
85
  };
72
- }, [location.pathname, location.search, delay]);
86
+ }, [location.pathname, delay, refreshWidgets]);
73
87
  }
74
88
 
75
89
  // src/components.tsx
90
+ import { useEffect as useEffect2 } from "react";
76
91
  import { jsx } from "react/jsx-runtime";
77
92
  function JudgemePreviewBadge({
78
93
  id,
79
- template = "product"
94
+ template = "product",
95
+ productTitle,
96
+ productHandle,
97
+ productImageUrl
80
98
  }) {
81
99
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
82
100
  return /* @__PURE__ */ jsx(
@@ -85,15 +103,61 @@ function JudgemePreviewBadge({
85
103
  className: "jdgm-widget jdgm-preview-badge",
86
104
  "data-id": shopifyId,
87
105
  "data-template": template,
88
- "data-auto-install": "false"
106
+ "data-auto-install": "false",
107
+ ...productTitle && { "data-product-title": productTitle },
108
+ ...productHandle && { "data-product-handle": productHandle },
109
+ ...productImageUrl && { "data-product-url": productImageUrl }
89
110
  }
90
111
  );
91
112
  }
92
113
  function JudgemeReviewWidget({
93
- id
114
+ id,
115
+ productTitle,
116
+ productHandle,
117
+ productImageUrl,
118
+ productDescription
94
119
  }) {
95
120
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
96
- return /* @__PURE__ */ jsx("div", { className: "jdgm-widget jdgm-review-widget", "data-id": shopifyId });
121
+ useEffect2(() => {
122
+ if (typeof window !== "undefined" && productTitle) {
123
+ if (!window.jdgm) {
124
+ window.jdgm = {};
125
+ }
126
+ window.jdgm.productData = {
127
+ id: shopifyId,
128
+ title: productTitle,
129
+ handle: productHandle || "",
130
+ description: productDescription || "",
131
+ image: productImageUrl || ""
132
+ };
133
+ window.JDGM_PRODUCT = {
134
+ id: shopifyId,
135
+ title: productTitle,
136
+ handle: productHandle || "",
137
+ description: productDescription || "",
138
+ url: productImageUrl || ""
139
+ };
140
+ }
141
+ }, [
142
+ shopifyId,
143
+ productTitle,
144
+ productHandle,
145
+ productImageUrl,
146
+ productDescription
147
+ ]);
148
+ return /* @__PURE__ */ jsx(
149
+ "div",
150
+ {
151
+ className: "jdgm-widget jdgm-review-widget",
152
+ "data-id": shopifyId,
153
+ ...productTitle && { "data-product-title": productTitle },
154
+ ...productHandle && { "data-product-handle": productHandle },
155
+ ...productImageUrl && { "data-product-url": productImageUrl },
156
+ ...productDescription && {
157
+ "data-product-description": productDescription
158
+ }
159
+ }
160
+ );
97
161
  }
98
162
  function JudgemeCarousel() {
99
163
  return /* @__PURE__ */ jsx("div", { className: "jdgm-widget jdgm-carousel-widget" });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "judgeme-hydrogen-fixed",
3
- "version": "1.0.0",
4
- "description": "Fixed version of @judgeme/shopify-hydrogen with proper React hooks implementation. Fixes infinite refresh loop bug.",
3
+ "version": "1.0.1",
4
+ "description": "Fixed version of @judgeme/shopify-hydrogen for Hydrogen/Oxygen. Fixes infinite refresh loops caused by installed.js and improper React hooks.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "README.md",
18
+ "CHANGELOG.md",
18
19
  "LICENSE"
19
20
  ],
20
21
  "scripts": {
@@ -27,13 +28,23 @@
27
28
  "judge.me",
28
29
  "shopify",
29
30
  "hydrogen",
31
+ "oxygen",
30
32
  "react",
31
33
  "reviews",
32
34
  "remix",
33
- "bug-fix"
35
+ "bug-fix",
36
+ "refresh-loop-fix"
34
37
  ],
35
38
  "author": "Ben Goodman",
36
39
  "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/ben-goodman-uk/judgeme-hydrogen-fixed"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/ben-goodman-uk/judgeme-hydrogen-fixed/issues"
46
+ },
47
+ "homepage": "https://github.com/ben-goodman-uk/judgeme-hydrogen-fixed#readme",
37
48
  "peerDependencies": {
38
49
  "react": ">=18.0.0",
39
50
  "@remix-run/react": ">=2.0.0"