medusa-product-helper 0.0.5 → 0.0.7

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.
@@ -988,6 +988,129 @@ const ValueField = ({
988
988
  adminSdk.defineWidgetConfig({
989
989
  zone: "product.details.after"
990
990
  });
991
+ const fetchJson = async (path) => {
992
+ const response = await fetch(path, {
993
+ credentials: "include"
994
+ });
995
+ const payload = await response.json().catch(() => null);
996
+ if (!response.ok) {
997
+ throw new Error(
998
+ (payload == null ? void 0 : payload.message) ?? "Unable to load wishlist statistics from the server"
999
+ );
1000
+ }
1001
+ return payload;
1002
+ };
1003
+ const ProductWishlistStatsWidget = ({ data }) => {
1004
+ const productId = data == null ? void 0 : data.id;
1005
+ const {
1006
+ data: productStats,
1007
+ isPending: isProductStatsPending,
1008
+ isError: isProductStatsError,
1009
+ error: productStatsError
1010
+ } = reactQuery.useQuery({
1011
+ queryKey: ["wishlist", "product", productId],
1012
+ enabled: Boolean(productId),
1013
+ queryFn: () => fetchJson(
1014
+ `/admin/wishlist/stats?product_id=${productId}`
1015
+ ),
1016
+ refetchInterval: 6e4
1017
+ });
1018
+ const {
1019
+ data: allStats,
1020
+ isPending: isAllStatsPending,
1021
+ isError: isAllStatsError,
1022
+ error: allStatsError
1023
+ } = reactQuery.useQuery({
1024
+ queryKey: ["wishlist", "stats"],
1025
+ queryFn: () => fetchJson("/admin/wishlist/stats"),
1026
+ staleTime: 6e4
1027
+ });
1028
+ const topFive = react.useMemo(() => {
1029
+ var _a;
1030
+ if (!((_a = allStats == null ? void 0 : allStats.stats) == null ? void 0 : _a.length)) {
1031
+ return [];
1032
+ }
1033
+ return [...allStats.stats].sort((a, b) => b.wishlist_count - a.wishlist_count).slice(0, 5);
1034
+ }, [allStats]);
1035
+ const productWishlistCount = (productStats == null ? void 0 : productStats.wishlist_count) ?? 0;
1036
+ const productIsTopWishlisted = topFive.some(
1037
+ (stat) => stat.product_id === productId
1038
+ );
1039
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
1040
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
1041
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Wishlist performance" }),
1042
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Track how often this product appears in customer wishlists and see the current top performers." })
1043
+ ] }),
1044
+ !productId ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "info", label: "Product not loaded yet", children: "Open a product detail record to view wishlist insights." }) : isProductStatsPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[96px] w-full rounded-lg" }) : isProductStatsError ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "error", label: "Unable to load product stats", children: productStatsError instanceof Error ? productStatsError.message : "Unknown error" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 rounded-lg border border-ui-border-base bg-ui-bg-base p-4", children: [
1045
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1046
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "This product" }),
1047
+ productIsTopWishlisted && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", color: "green", children: "Top 5" })
1048
+ ] }),
1049
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1050
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", className: "text-[32px] leading-none", children: productWishlistCount }),
1051
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: productWishlistCount === 1 ? "customer has saved this product." : "customers have saved this product." })
1052
+ ] })
1053
+ ] }),
1054
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 rounded-lg border border-ui-border-base bg-ui-bg-subtle p-4", children: [
1055
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
1056
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1057
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "Storewide insights" }),
1058
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Top wishlisted products" })
1059
+ ] }),
1060
+ productId && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: productIsTopWishlisted ? "In top 5" : "Not in top 5" })
1061
+ ] }),
1062
+ isAllStatsPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full rounded-lg" }) : isAllStatsError ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "error", label: "Unable to load storewide stats", children: allStatsError instanceof Error ? allStatsError.message : "Unknown error" }) : !topFive.length ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "info", label: "No wishlist activity yet", children: "Customers have not saved any products to their wishlists yet. Once they do, the most popular products will show up here." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
1063
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-field", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1064
+ /* @__PURE__ */ jsxRuntime.jsx(
1065
+ "th",
1066
+ {
1067
+ scope: "col",
1068
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1069
+ children: "#"
1070
+ }
1071
+ ),
1072
+ /* @__PURE__ */ jsxRuntime.jsx(
1073
+ "th",
1074
+ {
1075
+ scope: "col",
1076
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1077
+ children: "Product ID"
1078
+ }
1079
+ ),
1080
+ /* @__PURE__ */ jsxRuntime.jsx(
1081
+ "th",
1082
+ {
1083
+ scope: "col",
1084
+ className: "px-4 py-2 text-right text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1085
+ children: "Wishlists"
1086
+ }
1087
+ )
1088
+ ] }) }),
1089
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: topFive.map((stat, index) => /* @__PURE__ */ jsxRuntime.jsxs(
1090
+ "tr",
1091
+ {
1092
+ className: stat.product_id === productId ? "bg-ui-bg-subtle" : "bg-ui-bg-base",
1093
+ children: [
1094
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "px-4 py-3 text-sm font-medium text-ui-fg-subtle", children: [
1095
+ "#",
1096
+ index + 1
1097
+ ] }),
1098
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1099
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-mono text-sm", children: stat.product_id }),
1100
+ stat.product_id === productId && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "green", children: "Current" })
1101
+ ] }) }),
1102
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right text-sm font-semibold", children: stat.wishlist_count.toLocaleString() })
1103
+ ]
1104
+ },
1105
+ stat.product_id
1106
+ )) })
1107
+ ] }) })
1108
+ ] })
1109
+ ] });
1110
+ };
1111
+ adminSdk.defineWidgetConfig({
1112
+ zone: "product.details.side.after"
1113
+ });
991
1114
  const i18nTranslations0 = {};
992
1115
  const widgetModule = { widgets: [
993
1116
  {
@@ -1005,6 +1128,10 @@ const widgetModule = { widgets: [
1005
1128
  {
1006
1129
  Component: ProductMetadataTableWidget,
1007
1130
  zone: ["product.details.after"]
1131
+ },
1132
+ {
1133
+ Component: ProductWishlistStatsWidget,
1134
+ zone: ["product.details.side.after"]
1008
1135
  }
1009
1136
  ] };
1010
1137
  const routeModule = {
@@ -987,6 +987,129 @@ const ValueField = ({
987
987
  defineWidgetConfig({
988
988
  zone: "product.details.after"
989
989
  });
990
+ const fetchJson = async (path) => {
991
+ const response = await fetch(path, {
992
+ credentials: "include"
993
+ });
994
+ const payload = await response.json().catch(() => null);
995
+ if (!response.ok) {
996
+ throw new Error(
997
+ (payload == null ? void 0 : payload.message) ?? "Unable to load wishlist statistics from the server"
998
+ );
999
+ }
1000
+ return payload;
1001
+ };
1002
+ const ProductWishlistStatsWidget = ({ data }) => {
1003
+ const productId = data == null ? void 0 : data.id;
1004
+ const {
1005
+ data: productStats,
1006
+ isPending: isProductStatsPending,
1007
+ isError: isProductStatsError,
1008
+ error: productStatsError
1009
+ } = useQuery({
1010
+ queryKey: ["wishlist", "product", productId],
1011
+ enabled: Boolean(productId),
1012
+ queryFn: () => fetchJson(
1013
+ `/admin/wishlist/stats?product_id=${productId}`
1014
+ ),
1015
+ refetchInterval: 6e4
1016
+ });
1017
+ const {
1018
+ data: allStats,
1019
+ isPending: isAllStatsPending,
1020
+ isError: isAllStatsError,
1021
+ error: allStatsError
1022
+ } = useQuery({
1023
+ queryKey: ["wishlist", "stats"],
1024
+ queryFn: () => fetchJson("/admin/wishlist/stats"),
1025
+ staleTime: 6e4
1026
+ });
1027
+ const topFive = useMemo(() => {
1028
+ var _a;
1029
+ if (!((_a = allStats == null ? void 0 : allStats.stats) == null ? void 0 : _a.length)) {
1030
+ return [];
1031
+ }
1032
+ return [...allStats.stats].sort((a, b) => b.wishlist_count - a.wishlist_count).slice(0, 5);
1033
+ }, [allStats]);
1034
+ const productWishlistCount = (productStats == null ? void 0 : productStats.wishlist_count) ?? 0;
1035
+ const productIsTopWishlisted = topFive.some(
1036
+ (stat) => stat.product_id === productId
1037
+ );
1038
+ return /* @__PURE__ */ jsxs(Container, { className: "flex flex-col gap-y-4", children: [
1039
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-y-1", children: [
1040
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Wishlist performance" }),
1041
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Track how often this product appears in customer wishlists and see the current top performers." })
1042
+ ] }),
1043
+ !productId ? /* @__PURE__ */ jsx(InlineTip, { variant: "info", label: "Product not loaded yet", children: "Open a product detail record to view wishlist insights." }) : isProductStatsPending ? /* @__PURE__ */ jsx(Skeleton, { className: "h-[96px] w-full rounded-lg" }) : isProductStatsError ? /* @__PURE__ */ jsx(InlineTip, { variant: "error", label: "Unable to load product stats", children: productStatsError instanceof Error ? productStatsError.message : "Unknown error" }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2 rounded-lg border border-ui-border-base bg-ui-bg-base p-4", children: [
1044
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
1045
+ /* @__PURE__ */ jsx(Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "This product" }),
1046
+ productIsTopWishlisted && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", rounded: "full", color: "green", children: "Top 5" })
1047
+ ] }),
1048
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
1049
+ /* @__PURE__ */ jsx(Heading, { level: "h1", className: "text-[32px] leading-none", children: productWishlistCount }),
1050
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: productWishlistCount === 1 ? "customer has saved this product." : "customers have saved this product." })
1051
+ ] })
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3 rounded-lg border border-ui-border-base bg-ui-bg-subtle p-4", children: [
1054
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
1055
+ /* @__PURE__ */ jsxs("div", { children: [
1056
+ /* @__PURE__ */ jsx(Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "Storewide insights" }),
1057
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Top wishlisted products" })
1058
+ ] }),
1059
+ productId && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: productIsTopWishlisted ? "In top 5" : "Not in top 5" })
1060
+ ] }),
1061
+ isAllStatsPending ? /* @__PURE__ */ jsx(Skeleton, { className: "h-[160px] w-full rounded-lg" }) : isAllStatsError ? /* @__PURE__ */ jsx(InlineTip, { variant: "error", label: "Unable to load storewide stats", children: allStatsError instanceof Error ? allStatsError.message : "Unknown error" }) : !topFive.length ? /* @__PURE__ */ jsx(InlineTip, { variant: "info", label: "No wishlist activity yet", children: "Customers have not saved any products to their wishlists yet. Once they do, the most popular products will show up here." }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
1062
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-field", children: /* @__PURE__ */ jsxs("tr", { children: [
1063
+ /* @__PURE__ */ jsx(
1064
+ "th",
1065
+ {
1066
+ scope: "col",
1067
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1068
+ children: "#"
1069
+ }
1070
+ ),
1071
+ /* @__PURE__ */ jsx(
1072
+ "th",
1073
+ {
1074
+ scope: "col",
1075
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1076
+ children: "Product ID"
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsx(
1080
+ "th",
1081
+ {
1082
+ scope: "col",
1083
+ className: "px-4 py-2 text-right text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1084
+ children: "Wishlists"
1085
+ }
1086
+ )
1087
+ ] }) }),
1088
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: topFive.map((stat, index) => /* @__PURE__ */ jsxs(
1089
+ "tr",
1090
+ {
1091
+ className: stat.product_id === productId ? "bg-ui-bg-subtle" : "bg-ui-bg-base",
1092
+ children: [
1093
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3 text-sm font-medium text-ui-fg-subtle", children: [
1094
+ "#",
1095
+ index + 1
1096
+ ] }),
1097
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
1098
+ /* @__PURE__ */ jsx(Text, { className: "font-mono text-sm", children: stat.product_id }),
1099
+ stat.product_id === productId && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "green", children: "Current" })
1100
+ ] }) }),
1101
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right text-sm font-semibold", children: stat.wishlist_count.toLocaleString() })
1102
+ ]
1103
+ },
1104
+ stat.product_id
1105
+ )) })
1106
+ ] }) })
1107
+ ] })
1108
+ ] });
1109
+ };
1110
+ defineWidgetConfig({
1111
+ zone: "product.details.side.after"
1112
+ });
990
1113
  const i18nTranslations0 = {};
991
1114
  const widgetModule = { widgets: [
992
1115
  {
@@ -1004,6 +1127,10 @@ const widgetModule = { widgets: [
1004
1127
  {
1005
1128
  Component: ProductMetadataTableWidget,
1006
1129
  zone: ["product.details.after"]
1130
+ },
1131
+ {
1132
+ Component: ProductWishlistStatsWidget,
1133
+ zone: ["product.details.side.after"]
1007
1134
  }
1008
1135
  ] };
1009
1136
  const routeModule = {
package/README.md CHANGED
@@ -194,6 +194,18 @@ inputs. No extra configuration is required beyond defining descriptors under
194
194
  `metadata.products.descriptors` or `metadata.categories.descriptors` in
195
195
  `medusa-config.ts`.
196
196
 
197
+ #### Wishlist Insights Widget
198
+
199
+ The admin extension also injects a **Wishlist performance** card into the product
200
+ details sidebar (`product.details.side.after`). The widget:
201
+
202
+ - Shows how many unique customers saved the current product.
203
+ - Highlights whether the product ranks within the most wishlisted items.
204
+ - Displays a live leaderboard (top 5) powered by `/admin/wishlist/stats`.
205
+
206
+ No additional configuration is required—install the plugin, run the wishlist
207
+ migration, and open any product inside Medusa Admin to see the UI.
208
+
197
209
  #### Promotion Window
198
210
 
199
211
  Configure how promotion dates are tracked using product metadata:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medusa-product-helper",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "A starter for Medusa plugins.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",