judgeme-hydrogen-fixed 1.0.0 → 1.0.2

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,46 @@
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.2] - 2026-01-06
9
+
10
+ ### Fixed
11
+
12
+ - Fixed duplicate widgets appearing on initial page load
13
+ - Route change effect now skips first render to avoid double initialization
14
+ - Initial refresh is now only triggered by the script loading effect
15
+ - Fixed widgets not displaying on initial page load
16
+ - Added retry mechanism to wait for widget elements in DOM before initializing
17
+
18
+ ## [1.0.1] - 2026-01-06
19
+
20
+ ### Changed
21
+
22
+ - Updated documentation and GitHub repository links
23
+
24
+ ## [1.0.0] - 2026-01-06
25
+
26
+ ### Added
27
+
28
+ - Initial release
29
+ - `useJudgeme` hook with proper dependency arrays (fixing the infinite render loop bug from the original package)
30
+ - All Judge.me widget components:
31
+ - `JudgemePreviewBadge` - Star rating badge for product cards
32
+ - `JudgemeReviewWidget` - Full review widget for product pages
33
+ - `JudgemeCarousel` - Review carousel widget
34
+ - `JudgemeReviewsTab` - Reviews tab widget
35
+ - `JudgemeAllReviewsRating` - Overall rating display
36
+ - `JudgemeVerifiedBadge` - Verified badge widget
37
+ - `JudgemeAllReviewsCount` - Total reviews count
38
+ - `JudgemeMedals` - Medals/badges widget
39
+ - Full TypeScript support
40
+ - Proper cleanup of timeouts to prevent memory leaks
41
+
42
+ ### Fixed
43
+
44
+ - Fixed infinite render loop caused by `useEffect` with no dependency array in original `@judgeme/shopify-hydrogen` package
45
+ - Fixed infinite refresh loops caused by `installed.js` when deployed to Shopify Oxygen
46
+ - 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,98 @@ 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 initialRefreshDoneRef = (0, import_react.useRef)(false);
50
+ const refreshWidgets = (0, import_react.useCallback)(() => {
51
+ if (typeof window === "undefined") return;
52
+ const attemptRefresh = (retriesLeft) => {
53
+ const widgets = document.querySelectorAll(".jdgm-widget");
54
+ if (widgets.length === 0 && retriesLeft > 0) {
55
+ setTimeout(() => attemptRefresh(retriesLeft - 1), 200);
56
+ return;
57
+ }
58
+ try {
59
+ if (typeof window.jdgm_preloader === "function" && !window.jdgmCacheServer) {
60
+ window.jdgm_preloader();
61
+ } else if (window.jdgmCacheServer) {
62
+ window.jdgmCacheServer.reloadAll();
63
+ }
64
+ } catch (e) {
65
+ console.warn("Judge.me: Error refreshing widgets", e);
66
+ }
67
+ };
68
+ attemptRefresh(10);
69
+ }, []);
47
70
  (0, import_react.useEffect)(() => {
48
- if (isLoadedRef.current) return;
71
+ if (typeof window === "undefined") return;
72
+ if (globalScriptsLoaded || isInitializedRef.current) return;
49
73
  if (!shopDomain || !publicToken || !cdnHost) {
50
74
  console.warn(
51
75
  "Judge.me: Missing config values for store domain, store public token, or cdn host"
52
76
  );
53
77
  return;
54
78
  }
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
- `;
79
+ globalScriptsLoaded = true;
80
+ isInitializedRef.current = true;
81
+ window.jdgm = window.jdgm || {};
82
+ window.jdgm.SHOP_DOMAIN = shopDomain;
83
+ window.jdgm.PLATFORM = "shopify";
84
+ window.jdgm.PUBLIC_TOKEN = publicToken;
63
85
  fetch(`${cdnHost}/widget_preloader.js`).then((res) => {
64
86
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
65
87
  return res.text();
66
88
  }).then((text) => {
67
- const preloaderFunction = `function jdgm_preloader(){${text}}`;
68
- const shopCredentialsScript = document.createElement("script");
69
89
  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");
90
+ preloaderScript.innerText = `function jdgm_preloader(){${text}}`;
91
+ document.head.appendChild(preloaderScript);
92
+ console.log("Judge.me preloader script loaded, initializing widgets...");
93
+ initialRefreshDoneRef.current = true;
94
+ refreshWidgets();
81
95
  }).catch((error) => {
82
96
  console.error("Judge.me: Failed to load scripts", error);
97
+ globalScriptsLoaded = false;
98
+ isInitializedRef.current = false;
83
99
  });
84
100
  return () => {
85
101
  if (rerenderTimeoutRef.current) {
86
102
  clearTimeout(rerenderTimeoutRef.current);
87
103
  }
88
104
  };
89
- }, [shopDomain, publicToken, cdnHost]);
105
+ }, [shopDomain, publicToken, cdnHost, refreshWidgets]);
90
106
  (0, import_react.useEffect)(() => {
107
+ if (typeof window === "undefined") return;
108
+ const normalizedPathname = location.pathname.replace(/\/$/, "") || "/";
109
+ if (lastPathnameRef.current === "") {
110
+ lastPathnameRef.current = normalizedPathname;
111
+ return;
112
+ }
113
+ if (lastPathnameRef.current === normalizedPathname) {
114
+ return;
115
+ }
116
+ lastPathnameRef.current = normalizedPathname;
91
117
  if (rerenderTimeoutRef.current) {
92
118
  clearTimeout(rerenderTimeoutRef.current);
93
119
  }
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);
120
+ rerenderTimeoutRef.current = setTimeout(refreshWidgets, delay);
101
121
  return () => {
102
122
  if (rerenderTimeoutRef.current) {
103
123
  clearTimeout(rerenderTimeoutRef.current);
104
124
  }
105
125
  };
106
- }, [location.pathname, location.search, delay]);
126
+ }, [location.pathname, delay, refreshWidgets]);
107
127
  }
108
128
 
109
129
  // src/components.tsx
130
+ var import_react3 = require("react");
110
131
  var import_jsx_runtime = require("react/jsx-runtime");
111
132
  function JudgemePreviewBadge({
112
133
  id,
113
- template = "product"
134
+ template = "product",
135
+ productTitle,
136
+ productHandle,
137
+ productImageUrl
114
138
  }) {
115
139
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
116
140
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -119,15 +143,61 @@ function JudgemePreviewBadge({
119
143
  className: "jdgm-widget jdgm-preview-badge",
120
144
  "data-id": shopifyId,
121
145
  "data-template": template,
122
- "data-auto-install": "false"
146
+ "data-auto-install": "false",
147
+ ...productTitle && { "data-product-title": productTitle },
148
+ ...productHandle && { "data-product-handle": productHandle },
149
+ ...productImageUrl && { "data-product-url": productImageUrl }
123
150
  }
124
151
  );
125
152
  }
126
153
  function JudgemeReviewWidget({
127
- id
154
+ id,
155
+ productTitle,
156
+ productHandle,
157
+ productImageUrl,
158
+ productDescription
128
159
  }) {
129
160
  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 });
161
+ (0, import_react3.useEffect)(() => {
162
+ if (typeof window !== "undefined" && productTitle) {
163
+ if (!window.jdgm) {
164
+ window.jdgm = {};
165
+ }
166
+ window.jdgm.productData = {
167
+ id: shopifyId,
168
+ title: productTitle,
169
+ handle: productHandle || "",
170
+ description: productDescription || "",
171
+ image: productImageUrl || ""
172
+ };
173
+ window.JDGM_PRODUCT = {
174
+ id: shopifyId,
175
+ title: productTitle,
176
+ handle: productHandle || "",
177
+ description: productDescription || "",
178
+ url: productImageUrl || ""
179
+ };
180
+ }
181
+ }, [
182
+ shopifyId,
183
+ productTitle,
184
+ productHandle,
185
+ productImageUrl,
186
+ productDescription
187
+ ]);
188
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
189
+ "div",
190
+ {
191
+ className: "jdgm-widget jdgm-review-widget",
192
+ "data-id": shopifyId,
193
+ ...productTitle && { "data-product-title": productTitle },
194
+ ...productHandle && { "data-product-handle": productHandle },
195
+ ...productImageUrl && { "data-product-url": productImageUrl },
196
+ ...productDescription && {
197
+ "data-product-description": productDescription
198
+ }
199
+ }
200
+ );
131
201
  }
132
202
  function JudgemeCarousel() {
133
203
  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,98 @@ 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 initialRefreshDoneRef = useRef(false);
16
+ const refreshWidgets = useCallback(() => {
17
+ if (typeof window === "undefined") return;
18
+ const attemptRefresh = (retriesLeft) => {
19
+ const widgets = document.querySelectorAll(".jdgm-widget");
20
+ if (widgets.length === 0 && retriesLeft > 0) {
21
+ setTimeout(() => attemptRefresh(retriesLeft - 1), 200);
22
+ return;
23
+ }
24
+ try {
25
+ if (typeof window.jdgm_preloader === "function" && !window.jdgmCacheServer) {
26
+ window.jdgm_preloader();
27
+ } else if (window.jdgmCacheServer) {
28
+ window.jdgmCacheServer.reloadAll();
29
+ }
30
+ } catch (e) {
31
+ console.warn("Judge.me: Error refreshing widgets", e);
32
+ }
33
+ };
34
+ attemptRefresh(10);
35
+ }, []);
13
36
  useEffect(() => {
14
- if (isLoadedRef.current) return;
37
+ if (typeof window === "undefined") return;
38
+ if (globalScriptsLoaded || isInitializedRef.current) return;
15
39
  if (!shopDomain || !publicToken || !cdnHost) {
16
40
  console.warn(
17
41
  "Judge.me: Missing config values for store domain, store public token, or cdn host"
18
42
  );
19
43
  return;
20
44
  }
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
- `;
45
+ globalScriptsLoaded = true;
46
+ isInitializedRef.current = true;
47
+ window.jdgm = window.jdgm || {};
48
+ window.jdgm.SHOP_DOMAIN = shopDomain;
49
+ window.jdgm.PLATFORM = "shopify";
50
+ window.jdgm.PUBLIC_TOKEN = publicToken;
29
51
  fetch(`${cdnHost}/widget_preloader.js`).then((res) => {
30
52
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
31
53
  return res.text();
32
54
  }).then((text) => {
33
- const preloaderFunction = `function jdgm_preloader(){${text}}`;
34
- const shopCredentialsScript = document.createElement("script");
35
55
  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");
56
+ preloaderScript.innerText = `function jdgm_preloader(){${text}}`;
57
+ document.head.appendChild(preloaderScript);
58
+ console.log("Judge.me preloader script loaded, initializing widgets...");
59
+ initialRefreshDoneRef.current = true;
60
+ refreshWidgets();
47
61
  }).catch((error) => {
48
62
  console.error("Judge.me: Failed to load scripts", error);
63
+ globalScriptsLoaded = false;
64
+ isInitializedRef.current = false;
49
65
  });
50
66
  return () => {
51
67
  if (rerenderTimeoutRef.current) {
52
68
  clearTimeout(rerenderTimeoutRef.current);
53
69
  }
54
70
  };
55
- }, [shopDomain, publicToken, cdnHost]);
71
+ }, [shopDomain, publicToken, cdnHost, refreshWidgets]);
56
72
  useEffect(() => {
73
+ if (typeof window === "undefined") return;
74
+ const normalizedPathname = location.pathname.replace(/\/$/, "") || "/";
75
+ if (lastPathnameRef.current === "") {
76
+ lastPathnameRef.current = normalizedPathname;
77
+ return;
78
+ }
79
+ if (lastPathnameRef.current === normalizedPathname) {
80
+ return;
81
+ }
82
+ lastPathnameRef.current = normalizedPathname;
57
83
  if (rerenderTimeoutRef.current) {
58
84
  clearTimeout(rerenderTimeoutRef.current);
59
85
  }
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);
86
+ rerenderTimeoutRef.current = setTimeout(refreshWidgets, delay);
67
87
  return () => {
68
88
  if (rerenderTimeoutRef.current) {
69
89
  clearTimeout(rerenderTimeoutRef.current);
70
90
  }
71
91
  };
72
- }, [location.pathname, location.search, delay]);
92
+ }, [location.pathname, delay, refreshWidgets]);
73
93
  }
74
94
 
75
95
  // src/components.tsx
96
+ import { useEffect as useEffect2 } from "react";
76
97
  import { jsx } from "react/jsx-runtime";
77
98
  function JudgemePreviewBadge({
78
99
  id,
79
- template = "product"
100
+ template = "product",
101
+ productTitle,
102
+ productHandle,
103
+ productImageUrl
80
104
  }) {
81
105
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
82
106
  return /* @__PURE__ */ jsx(
@@ -85,15 +109,61 @@ function JudgemePreviewBadge({
85
109
  className: "jdgm-widget jdgm-preview-badge",
86
110
  "data-id": shopifyId,
87
111
  "data-template": template,
88
- "data-auto-install": "false"
112
+ "data-auto-install": "false",
113
+ ...productTitle && { "data-product-title": productTitle },
114
+ ...productHandle && { "data-product-handle": productHandle },
115
+ ...productImageUrl && { "data-product-url": productImageUrl }
89
116
  }
90
117
  );
91
118
  }
92
119
  function JudgemeReviewWidget({
93
- id
120
+ id,
121
+ productTitle,
122
+ productHandle,
123
+ productImageUrl,
124
+ productDescription
94
125
  }) {
95
126
  const shopifyId = id ? id.replace("gid://shopify/Product/", "") : "";
96
- return /* @__PURE__ */ jsx("div", { className: "jdgm-widget jdgm-review-widget", "data-id": shopifyId });
127
+ useEffect2(() => {
128
+ if (typeof window !== "undefined" && productTitle) {
129
+ if (!window.jdgm) {
130
+ window.jdgm = {};
131
+ }
132
+ window.jdgm.productData = {
133
+ id: shopifyId,
134
+ title: productTitle,
135
+ handle: productHandle || "",
136
+ description: productDescription || "",
137
+ image: productImageUrl || ""
138
+ };
139
+ window.JDGM_PRODUCT = {
140
+ id: shopifyId,
141
+ title: productTitle,
142
+ handle: productHandle || "",
143
+ description: productDescription || "",
144
+ url: productImageUrl || ""
145
+ };
146
+ }
147
+ }, [
148
+ shopifyId,
149
+ productTitle,
150
+ productHandle,
151
+ productImageUrl,
152
+ productDescription
153
+ ]);
154
+ return /* @__PURE__ */ jsx(
155
+ "div",
156
+ {
157
+ className: "jdgm-widget jdgm-review-widget",
158
+ "data-id": shopifyId,
159
+ ...productTitle && { "data-product-title": productTitle },
160
+ ...productHandle && { "data-product-handle": productHandle },
161
+ ...productImageUrl && { "data-product-url": productImageUrl },
162
+ ...productDescription && {
163
+ "data-product-description": productDescription
164
+ }
165
+ }
166
+ );
97
167
  }
98
168
  function JudgemeCarousel() {
99
169
  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.2",
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"