medusa-contact-us 0.0.2 → 0.0.8

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.
Files changed (23) hide show
  1. package/.medusa/server/src/admin/index.js +176 -1
  2. package/.medusa/server/src/admin/index.mjs +176 -1
  3. package/.medusa/server/src/api/admin/contact-email-subscriptions/route.js +29 -0
  4. package/.medusa/server/src/api/admin/contact-email-subscriptions/validators.js +12 -0
  5. package/.medusa/server/src/api/store/contact-email-subscriptions/route.js +19 -0
  6. package/.medusa/server/src/api/store/contact-email-subscriptions/validators.js +15 -0
  7. package/.medusa/server/src/constants.js +3 -2
  8. package/.medusa/server/src/helpers/__tests__/contact-subscription.test.js +109 -0
  9. package/.medusa/server/src/helpers/__tests__/submit-contact-request.test.js +33 -1
  10. package/.medusa/server/src/helpers/base-client.js +36 -0
  11. package/.medusa/server/src/helpers/contact-subscription.js +45 -0
  12. package/.medusa/server/src/helpers/index.js +5 -2
  13. package/.medusa/server/src/helpers/submit-contact-request.js +8 -29
  14. package/.medusa/server/src/index.js +7 -2
  15. package/.medusa/server/src/modules/contact-requests/migrations/Migration20241124090000.js +7 -2
  16. package/.medusa/server/src/modules/contact-requests/models/contact-request-comment.js +1 -2
  17. package/.medusa/server/src/modules/contact-requests/models/contact-request.js +1 -3
  18. package/.medusa/server/src/modules/contact-subscriptions/index.js +15 -0
  19. package/.medusa/server/src/modules/contact-subscriptions/migrations/Migration20241126103000.js +38 -0
  20. package/.medusa/server/src/modules/contact-subscriptions/models/contact-email-subscription.js +13 -0
  21. package/.medusa/server/src/modules/contact-subscriptions/service.js +57 -0
  22. package/README.md +132 -12
  23. package/package.json +2 -1
@@ -1,10 +1,175 @@
1
1
  "use strict";
2
2
  const jsxRuntime = require("react/jsx-runtime");
3
3
  const react = require("react");
4
- const reactRouterDom = require("react-router-dom");
5
4
  const adminSdk = require("@medusajs/admin-sdk");
6
5
  const ui = require("@medusajs/ui");
7
6
  const icons = require("@medusajs/icons");
7
+ const reactRouterDom = require("react-router-dom");
8
+ const useDebounce$1 = (value, delay) => {
9
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
10
+ react.useEffect(() => {
11
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
12
+ return () => clearTimeout(handler);
13
+ }, [value, delay]);
14
+ return debouncedValue;
15
+ };
16
+ const badgeClass = (status) => {
17
+ if (status === "subscribed") {
18
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
19
+ }
20
+ return "bg-ui-tag-red-bg text-ui-tag-red-text";
21
+ };
22
+ const statusFilters = [
23
+ { value: "all", label: "All" },
24
+ { value: "subscribed", label: "Subscribed" },
25
+ { value: "unsubscribed", label: "Unsubscribed" }
26
+ ];
27
+ const ContactEmailSubscriptionsPage = () => {
28
+ const [items, setItems] = react.useState([]);
29
+ const [statusFilter, setStatusFilter] = react.useState("all");
30
+ const [query, setQuery] = react.useState("");
31
+ const debouncedQuery = useDebounce$1(query, 300);
32
+ const [isLoading, setIsLoading] = react.useState(true);
33
+ const [isFetchingMore, setIsFetchingMore] = react.useState(false);
34
+ const [error, setError] = react.useState(null);
35
+ const [offset, setOffset] = react.useState(0);
36
+ const [count, setCount] = react.useState(0);
37
+ const limit = 50;
38
+ const loadSubscriptions = react.useCallback(
39
+ async (nextOffset, replace = false) => {
40
+ var _a;
41
+ try {
42
+ if (replace) {
43
+ setIsLoading(true);
44
+ } else {
45
+ setIsFetchingMore(true);
46
+ }
47
+ setError(null);
48
+ const params = new URLSearchParams();
49
+ params.set("limit", String(limit));
50
+ params.set("offset", String(nextOffset));
51
+ if (statusFilter !== "all") {
52
+ params.set("status", statusFilter);
53
+ }
54
+ if (debouncedQuery.trim()) {
55
+ params.set("q", debouncedQuery.trim());
56
+ }
57
+ const response = await fetch(
58
+ `/admin/contact-email-subscriptions?${params.toString()}`,
59
+ { credentials: "include" }
60
+ );
61
+ if (!response.ok) {
62
+ const message = await response.text();
63
+ throw new Error(message || "Unable to load subscriptions");
64
+ }
65
+ const payload = await response.json();
66
+ setCount(payload.count ?? 0);
67
+ setOffset(nextOffset + (((_a = payload.subscriptions) == null ? void 0 : _a.length) ?? 0));
68
+ setItems(
69
+ (prev) => replace ? payload.subscriptions ?? [] : [...prev, ...payload.subscriptions ?? []]
70
+ );
71
+ } catch (loadError) {
72
+ const message = loadError instanceof Error ? loadError.message : "Unable to load subscriptions";
73
+ setError(message);
74
+ } finally {
75
+ setIsLoading(false);
76
+ setIsFetchingMore(false);
77
+ }
78
+ },
79
+ [statusFilter, debouncedQuery]
80
+ );
81
+ react.useEffect(() => {
82
+ void loadSubscriptions(0, true);
83
+ }, [statusFilter, debouncedQuery, loadSubscriptions]);
84
+ const hasMore = react.useMemo(() => offset < count, [offset, count]);
85
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
86
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
87
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
88
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact email list" }),
89
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Track opt-ins and unsubscribes collected from the storefront helper." })
90
+ ] }),
91
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", onClick: () => loadSubscriptions(0, true), children: "Refresh" })
92
+ ] }),
93
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ ui.Input,
96
+ {
97
+ placeholder: "Search email",
98
+ value: query,
99
+ onChange: (event) => setQuery(event.target.value),
100
+ className: "md:max-w-sm"
101
+ }
102
+ ),
103
+ /* @__PURE__ */ jsxRuntime.jsx(
104
+ "select",
105
+ {
106
+ value: statusFilter,
107
+ onChange: (event) => setStatusFilter(event.target.value),
108
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
109
+ children: statusFilters.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
110
+ }
111
+ )
112
+ ] }),
113
+ error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
114
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
115
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
116
+ ui.Button,
117
+ {
118
+ variant: "secondary",
119
+ onClick: () => loadSubscriptions(0, true),
120
+ children: "Try again"
121
+ }
122
+ ) })
123
+ ] }) : null,
124
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading email list..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
125
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-xl", children: "No entries yet" }),
126
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Submissions created through the storefront helper will appear here with their latest consent state." })
127
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
128
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
129
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
130
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
131
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
132
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Last updated" })
133
+ ] }) }),
134
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((subscription) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "hover:bg-ui-bg-subtle/60", children: [
135
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5", children: [
136
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: subscription.email }),
137
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: subscription.id })
138
+ ] }) }),
139
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "px-4 py-4", children: [
140
+ /* @__PURE__ */ jsxRuntime.jsx(
141
+ ui.Badge,
142
+ {
143
+ size: "2xsmall",
144
+ className: `uppercase ${badgeClass(subscription.status)}`,
145
+ children: subscription.status
146
+ }
147
+ ),
148
+ subscription.unsubscribed_at ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "mt-1 block text-ui-fg-subtle", children: [
149
+ "Unsubscribed on",
150
+ " ",
151
+ new Date(subscription.unsubscribed_at).toLocaleString()
152
+ ] }) : null
153
+ ] }),
154
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: subscription.source ?? "storefront" }),
155
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(subscription.updated_at).toLocaleString() })
156
+ ] }, subscription.id)) })
157
+ ] }) }),
158
+ hasMore ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
159
+ ui.Button,
160
+ {
161
+ variant: "secondary",
162
+ isLoading: isFetchingMore,
163
+ onClick: () => loadSubscriptions(offset, false),
164
+ children: "Load more"
165
+ }
166
+ ) }) : null
167
+ ] }) });
168
+ };
169
+ const config$1 = adminSdk.defineRouteConfig({
170
+ label: "Contact email list",
171
+ icon: icons.Envelope
172
+ });
8
173
  const useDebounce = (value, delay) => {
9
174
  const [debouncedValue, setDebouncedValue] = react.useState(value);
10
175
  react.useEffect(() => {
@@ -487,6 +652,10 @@ const i18nTranslations0 = {
487
652
  const widgetModule = { widgets: [] };
488
653
  const routeModule = {
489
654
  routes: [
655
+ {
656
+ Component: ContactEmailSubscriptionsPage,
657
+ path: "/contact-email-subscriptions"
658
+ },
490
659
  {
491
660
  Component: ContactRequestsPage,
492
661
  path: "/contact-requests"
@@ -499,6 +668,12 @@ const routeModule = {
499
668
  };
500
669
  const menuItemModule = {
501
670
  menuItems: [
671
+ {
672
+ label: config$1.label,
673
+ icon: config$1.icon,
674
+ path: "/contact-email-subscriptions",
675
+ nested: void 0
676
+ },
502
677
  {
503
678
  label: config.label,
504
679
  icon: config.icon,
@@ -1,9 +1,174 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useEffect, useMemo } from "react";
3
- import { Link, useParams, useNavigate } from "react-router-dom";
4
3
  import { defineRouteConfig } from "@medusajs/admin-sdk";
5
4
  import { Container, Heading, Text, Button, Input, Badge, toast, Textarea } from "@medusajs/ui";
6
5
  import { Envelope, ArrowUturnLeft } from "@medusajs/icons";
6
+ import { Link, useParams, useNavigate } from "react-router-dom";
7
+ const useDebounce$1 = (value, delay) => {
8
+ const [debouncedValue, setDebouncedValue] = useState(value);
9
+ useEffect(() => {
10
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
11
+ return () => clearTimeout(handler);
12
+ }, [value, delay]);
13
+ return debouncedValue;
14
+ };
15
+ const badgeClass = (status) => {
16
+ if (status === "subscribed") {
17
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
18
+ }
19
+ return "bg-ui-tag-red-bg text-ui-tag-red-text";
20
+ };
21
+ const statusFilters = [
22
+ { value: "all", label: "All" },
23
+ { value: "subscribed", label: "Subscribed" },
24
+ { value: "unsubscribed", label: "Unsubscribed" }
25
+ ];
26
+ const ContactEmailSubscriptionsPage = () => {
27
+ const [items, setItems] = useState([]);
28
+ const [statusFilter, setStatusFilter] = useState("all");
29
+ const [query, setQuery] = useState("");
30
+ const debouncedQuery = useDebounce$1(query, 300);
31
+ const [isLoading, setIsLoading] = useState(true);
32
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
33
+ const [error, setError] = useState(null);
34
+ const [offset, setOffset] = useState(0);
35
+ const [count, setCount] = useState(0);
36
+ const limit = 50;
37
+ const loadSubscriptions = useCallback(
38
+ async (nextOffset, replace = false) => {
39
+ var _a;
40
+ try {
41
+ if (replace) {
42
+ setIsLoading(true);
43
+ } else {
44
+ setIsFetchingMore(true);
45
+ }
46
+ setError(null);
47
+ const params = new URLSearchParams();
48
+ params.set("limit", String(limit));
49
+ params.set("offset", String(nextOffset));
50
+ if (statusFilter !== "all") {
51
+ params.set("status", statusFilter);
52
+ }
53
+ if (debouncedQuery.trim()) {
54
+ params.set("q", debouncedQuery.trim());
55
+ }
56
+ const response = await fetch(
57
+ `/admin/contact-email-subscriptions?${params.toString()}`,
58
+ { credentials: "include" }
59
+ );
60
+ if (!response.ok) {
61
+ const message = await response.text();
62
+ throw new Error(message || "Unable to load subscriptions");
63
+ }
64
+ const payload = await response.json();
65
+ setCount(payload.count ?? 0);
66
+ setOffset(nextOffset + (((_a = payload.subscriptions) == null ? void 0 : _a.length) ?? 0));
67
+ setItems(
68
+ (prev) => replace ? payload.subscriptions ?? [] : [...prev, ...payload.subscriptions ?? []]
69
+ );
70
+ } catch (loadError) {
71
+ const message = loadError instanceof Error ? loadError.message : "Unable to load subscriptions";
72
+ setError(message);
73
+ } finally {
74
+ setIsLoading(false);
75
+ setIsFetchingMore(false);
76
+ }
77
+ },
78
+ [statusFilter, debouncedQuery]
79
+ );
80
+ useEffect(() => {
81
+ void loadSubscriptions(0, true);
82
+ }, [statusFilter, debouncedQuery, loadSubscriptions]);
83
+ const hasMore = useMemo(() => offset < count, [offset, count]);
84
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
85
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
86
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
87
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact email list" }),
88
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Track opt-ins and unsubscribes collected from the storefront helper." })
89
+ ] }),
90
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => loadSubscriptions(0, true), children: "Refresh" })
91
+ ] }),
92
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
93
+ /* @__PURE__ */ jsx(
94
+ Input,
95
+ {
96
+ placeholder: "Search email",
97
+ value: query,
98
+ onChange: (event) => setQuery(event.target.value),
99
+ className: "md:max-w-sm"
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(
103
+ "select",
104
+ {
105
+ value: statusFilter,
106
+ onChange: (event) => setStatusFilter(event.target.value),
107
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
108
+ children: statusFilters.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
109
+ }
110
+ )
111
+ ] }),
112
+ error ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
113
+ /* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
114
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(
115
+ Button,
116
+ {
117
+ variant: "secondary",
118
+ onClick: () => loadSubscriptions(0, true),
119
+ children: "Try again"
120
+ }
121
+ ) })
122
+ ] }) : null,
123
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading email list..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
124
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-xl", children: "No entries yet" }),
125
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Submissions created through the storefront helper will appear here with their latest consent state." })
126
+ ] }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
127
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
128
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
129
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
130
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
131
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Last updated" })
132
+ ] }) }),
133
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((subscription) => /* @__PURE__ */ jsxs("tr", { className: "hover:bg-ui-bg-subtle/60", children: [
134
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
135
+ /* @__PURE__ */ jsx("span", { children: subscription.email }),
136
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: subscription.id })
137
+ ] }) }),
138
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-4", children: [
139
+ /* @__PURE__ */ jsx(
140
+ Badge,
141
+ {
142
+ size: "2xsmall",
143
+ className: `uppercase ${badgeClass(subscription.status)}`,
144
+ children: subscription.status
145
+ }
146
+ ),
147
+ subscription.unsubscribed_at ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "mt-1 block text-ui-fg-subtle", children: [
148
+ "Unsubscribed on",
149
+ " ",
150
+ new Date(subscription.unsubscribed_at).toLocaleString()
151
+ ] }) : null
152
+ ] }),
153
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: subscription.source ?? "storefront" }),
154
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(subscription.updated_at).toLocaleString() })
155
+ ] }, subscription.id)) })
156
+ ] }) }),
157
+ hasMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
158
+ Button,
159
+ {
160
+ variant: "secondary",
161
+ isLoading: isFetchingMore,
162
+ onClick: () => loadSubscriptions(offset, false),
163
+ children: "Load more"
164
+ }
165
+ ) }) : null
166
+ ] }) });
167
+ };
168
+ const config$1 = defineRouteConfig({
169
+ label: "Contact email list",
170
+ icon: Envelope
171
+ });
7
172
  const useDebounce = (value, delay) => {
8
173
  const [debouncedValue, setDebouncedValue] = useState(value);
9
174
  useEffect(() => {
@@ -486,6 +651,10 @@ const i18nTranslations0 = {
486
651
  const widgetModule = { widgets: [] };
487
652
  const routeModule = {
488
653
  routes: [
654
+ {
655
+ Component: ContactEmailSubscriptionsPage,
656
+ path: "/contact-email-subscriptions"
657
+ },
489
658
  {
490
659
  Component: ContactRequestsPage,
491
660
  path: "/contact-requests"
@@ -498,6 +667,12 @@ const routeModule = {
498
667
  };
499
668
  const menuItemModule = {
500
669
  menuItems: [
670
+ {
671
+ label: config$1.label,
672
+ icon: config$1.icon,
673
+ path: "/contact-email-subscriptions",
674
+ nested: void 0
675
+ },
501
676
  {
502
677
  label: config.label,
503
678
  icon: config.icon,
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = void 0;
4
+ const constants_1 = require("../../../constants");
5
+ const validators_1 = require("./validators");
6
+ const GET = async (req, res) => {
7
+ const query = validators_1.AdminListContactEmailSubscriptionsSchema.parse(req.query ?? {});
8
+ const service = req.scope.resolve(constants_1.CONTACT_SUBSCRIPTION_MODULE);
9
+ const selector = {};
10
+ if (query.status) {
11
+ selector.status = query.status;
12
+ }
13
+ if (query.q) {
14
+ selector.email = query.q.trim().toLowerCase();
15
+ }
16
+ const [subscriptions, count] = await service.listWithFilters(selector, {
17
+ take: query.limit,
18
+ skip: query.offset,
19
+ order: { created_at: "DESC" },
20
+ });
21
+ res.status(200).json({
22
+ subscriptions,
23
+ count,
24
+ offset: query.offset,
25
+ limit: query.limit,
26
+ });
27
+ };
28
+ exports.GET = GET;
29
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtZW1haWwtc3Vic2NyaXB0aW9ucy9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSxrREFBZ0U7QUFFaEUsNkNBQXVFO0FBRWhFLE1BQU0sR0FBRyxHQUFHLEtBQUssRUFBRSxHQUFrQixFQUFFLEdBQW1CLEVBQUUsRUFBRTtJQUNuRSxNQUFNLEtBQUssR0FBRyxxREFBd0MsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUM3RSxNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDL0IsdUNBQTJCLENBQzVCLENBQUE7SUFFRCxNQUFNLFFBQVEsR0FBNEIsRUFBRSxDQUFBO0lBRTVDLElBQUksS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ2pCLFFBQVEsQ0FBQyxNQUFNLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQTtJQUNoQyxDQUFDO0lBQ0QsSUFBSSxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDWixRQUFRLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDL0MsQ0FBQztJQUVELE1BQU0sQ0FBQyxhQUFhLEVBQUUsS0FBSyxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsZUFBZSxDQUFDLFFBQVEsRUFBRTtRQUNyRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEtBQUs7UUFDakIsSUFBSSxFQUFFLEtBQUssQ0FBQyxNQUFNO1FBQ2xCLEtBQUssRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUU7S0FDOUIsQ0FBQyxDQUFBO0lBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsYUFBYTtRQUNiLEtBQUs7UUFDTCxNQUFNLEVBQUUsS0FBSyxDQUFDLE1BQU07UUFDcEIsS0FBSyxFQUFFLEtBQUssQ0FBQyxLQUFLO0tBQ25CLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQTtBQTNCWSxRQUFBLEdBQUcsT0EyQmYifQ==
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdminListContactEmailSubscriptionsSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const validators_1 = require("../../store/contact-email-subscriptions/validators");
6
+ exports.AdminListContactEmailSubscriptionsSchema = zod_1.z.object({
7
+ status: validators_1.ContactSubscriptionStatusEnum.optional(),
8
+ q: zod_1.z.string().optional(),
9
+ limit: zod_1.z.coerce.number().min(1).max(100).default(50),
10
+ offset: zod_1.z.coerce.number().min(0).default(0),
11
+ });
12
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9hcGkvYWRtaW4vY29udGFjdC1lbWFpbC1zdWJzY3JpcHRpb25zL3ZhbGlkYXRvcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsNkJBQXVCO0FBQ3ZCLG1GQUFrRztBQUVyRixRQUFBLHdDQUF3QyxHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDL0QsTUFBTSxFQUFFLDBDQUE2QixDQUFDLFFBQVEsRUFBRTtJQUNoRCxDQUFDLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtJQUN4QixLQUFLLEVBQUUsT0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7SUFDcEQsTUFBTSxFQUFFLE9BQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7Q0FDNUMsQ0FBQyxDQUFBIn0=
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = exports.AUTHENTICATE = void 0;
4
+ const constants_1 = require("../../../constants");
5
+ const validators_1 = require("./validators");
6
+ exports.AUTHENTICATE = false;
7
+ const POST = async (req, res) => {
8
+ const body = validators_1.StoreUpsertContactSubscriptionSchema.parse(req.body ?? {});
9
+ const service = req.scope.resolve(constants_1.CONTACT_SUBSCRIPTION_MODULE);
10
+ const subscription = await service.upsertSubscription({
11
+ email: body.email,
12
+ status: body.status,
13
+ metadata: body.metadata,
14
+ source: body.source ?? "storefront",
15
+ });
16
+ res.status(200).json({ subscription });
17
+ };
18
+ exports.POST = POST;
19
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL3N0b3JlL2NvbnRhY3QtZW1haWwtc3Vic2NyaXB0aW9ucy9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSxrREFBZ0U7QUFFaEUsNkNBQW1FO0FBRXRELFFBQUEsWUFBWSxHQUFHLEtBQUssQ0FBQTtBQUUxQixNQUFNLElBQUksR0FBRyxLQUFLLEVBQUUsR0FBa0IsRUFBRSxHQUFtQixFQUFFLEVBQUU7SUFDcEUsTUFBTSxJQUFJLEdBQUcsaURBQW9DLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUE7SUFFdkUsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQy9CLHVDQUEyQixDQUM1QixDQUFBO0lBRUQsTUFBTSxZQUFZLEdBQUcsTUFBTSxPQUFPLENBQUMsa0JBQWtCLENBQUM7UUFDcEQsS0FBSyxFQUFFLElBQUksQ0FBQyxLQUFLO1FBQ2pCLE1BQU0sRUFBRSxJQUFJLENBQUMsTUFBTTtRQUNuQixRQUFRLEVBQUUsSUFBSSxDQUFDLFFBQVE7UUFDdkIsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLElBQUksWUFBWTtLQUNwQyxDQUFDLENBQUE7SUFFRixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLFlBQVksRUFBRSxDQUFDLENBQUE7QUFDeEMsQ0FBQyxDQUFBO0FBZlksUUFBQSxJQUFJLFFBZWhCIn0=
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StoreUpsertContactSubscriptionSchema = exports.ContactSubscriptionStatusEnum = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.ContactSubscriptionStatusEnum = zod_1.z.enum([
6
+ "subscribed",
7
+ "unsubscribed",
8
+ ]);
9
+ exports.StoreUpsertContactSubscriptionSchema = zod_1.z.object({
10
+ email: zod_1.z.string().email(),
11
+ status: exports.ContactSubscriptionStatusEnum.optional(),
12
+ metadata: zod_1.z.record(zod_1.z.any()).optional(),
13
+ source: zod_1.z.string().optional(),
14
+ });
15
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9hcGkvc3RvcmUvY29udGFjdC1lbWFpbC1zdWJzY3JpcHRpb25zL3ZhbGlkYXRvcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsNkJBQXVCO0FBRVYsUUFBQSw2QkFBNkIsR0FBRyxPQUFDLENBQUMsSUFBSSxDQUFDO0lBQ2xELFlBQVk7SUFDWixjQUFjO0NBQ2YsQ0FBQyxDQUFBO0FBRVcsUUFBQSxvQ0FBb0MsR0FBRyxPQUFDLENBQUMsTUFBTSxDQUFDO0lBQzNELEtBQUssRUFBRSxPQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsS0FBSyxFQUFFO0lBQ3pCLE1BQU0sRUFBRSxxQ0FBNkIsQ0FBQyxRQUFRLEVBQUU7SUFDaEQsUUFBUSxFQUFFLE9BQUMsQ0FBQyxNQUFNLENBQUMsT0FBQyxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUMsUUFBUSxFQUFFO0lBQ3RDLE1BQU0sRUFBRSxPQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxFQUFFO0NBQzlCLENBQUMsQ0FBQSJ9
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONTACT_REQUEST_MODULE = void 0;
3
+ exports.CONTACT_SUBSCRIPTION_MODULE = exports.CONTACT_REQUEST_MODULE = void 0;
4
4
  exports.CONTACT_REQUEST_MODULE = "contact_requests";
5
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBYSxRQUFBLHNCQUFzQixHQUFHLGtCQUFrQixDQUFBIn0=
5
+ exports.CONTACT_SUBSCRIPTION_MODULE = "contact_email_subscriptions";
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBYSxRQUFBLHNCQUFzQixHQUFHLGtCQUFrQixDQUFBO0FBQzNDLFFBQUEsMkJBQTJCLEdBQUcsNkJBQTZCLENBQUEifQ==
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const contact_subscription_1 = require("../contact-subscription");
5
+ const buildPayload = () => ({
6
+ email: "sub@example.com",
7
+ status: "subscribed",
8
+ metadata: { channel: "footer" },
9
+ });
10
+ (0, vitest_1.describe)("upsertContactSubscription helper", () => {
11
+ const fetchCases = [
12
+ {
13
+ name: "uses provided fetch implementation when no client exists",
14
+ setup: () => {
15
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
16
+ ok: true,
17
+ json: () => Promise.resolve({
18
+ subscription: { id: "sub_1" },
19
+ }),
20
+ });
21
+ return {
22
+ fetchImpl: mockFetch,
23
+ expectedUrl: "https://demo.store/store/contact-email-subscriptions",
24
+ };
25
+ },
26
+ },
27
+ {
28
+ name: "throws when response is not ok",
29
+ setup: () => {
30
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
31
+ ok: false,
32
+ status: 400,
33
+ text: () => Promise.resolve("invalid"),
34
+ });
35
+ return {
36
+ fetchImpl: mockFetch,
37
+ expectedError: /invalid/i,
38
+ };
39
+ },
40
+ },
41
+ ];
42
+ fetchCases.forEach(({ name, setup }) => {
43
+ (0, vitest_1.it)(name, async () => {
44
+ const config = setup();
45
+ const { fetchImpl } = config;
46
+ if ("expectedError" in config) {
47
+ await (0, vitest_1.expect)((0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
48
+ fetchImpl,
49
+ baseUrl: "https://demo.store",
50
+ })).rejects.toMatchObject({
51
+ message: vitest_1.expect.stringMatching(config.expectedError),
52
+ });
53
+ return;
54
+ }
55
+ const result = await (0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
56
+ fetchImpl,
57
+ baseUrl: "https://demo.store",
58
+ });
59
+ (0, vitest_1.expect)(result).toEqual({ subscription: { id: "sub_1" } });
60
+ (0, vitest_1.expect)(fetchImpl).toHaveBeenCalledWith(config.expectedUrl, vitest_1.expect.anything());
61
+ });
62
+ });
63
+ (0, vitest_1.it)("uses medusa client when provided", async () => {
64
+ const mockClient = {
65
+ request: vitest_1.vi.fn().mockResolvedValue({
66
+ subscription: { id: "sub_2" },
67
+ }),
68
+ };
69
+ const result = await (0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
70
+ client: mockClient,
71
+ });
72
+ (0, vitest_1.expect)(result.subscription.id).toBe("sub_2");
73
+ (0, vitest_1.expect)(mockClient.request).toHaveBeenCalledWith("/store/contact-email-subscriptions", vitest_1.expect.objectContaining({
74
+ method: "POST",
75
+ }));
76
+ });
77
+ (0, vitest_1.it)("attaches publishable API key to headers", async () => {
78
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
79
+ ok: true,
80
+ json: () => Promise.resolve({
81
+ subscription: { id: "sub_3" },
82
+ }),
83
+ });
84
+ await (0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
85
+ fetchImpl: mockFetch,
86
+ baseUrl: "https://demo.store",
87
+ publishableApiKey: "pk_subscribe",
88
+ });
89
+ const [, init] = mockFetch.mock.calls[0];
90
+ (0, vitest_1.expect)(init?.headers).toMatchObject({
91
+ "x-publishable-api-key": "pk_subscribe",
92
+ });
93
+ });
94
+ (0, vitest_1.it)("allows helper factory to preconfigure options", async () => {
95
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
96
+ ok: true,
97
+ json: () => Promise.resolve({
98
+ subscription: { id: "sub_4" },
99
+ }),
100
+ });
101
+ const updateSubscription = (0, contact_subscription_1.createUpsertContactSubscription)({
102
+ fetchImpl: mockFetch,
103
+ baseUrl: "https://demo.store",
104
+ });
105
+ const result = await updateSubscription(buildPayload());
106
+ (0, vitest_1.expect)(result.subscription.id).toBe("sub_4");
107
+ });
108
+ });
109
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGFjdC1zdWJzY3JpcHRpb24udGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9oZWxwZXJzL19fdGVzdHNfXy9jb250YWN0LXN1YnNjcmlwdGlvbi50ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsbUNBQWlEO0FBQ2pELGtFQUdnQztBQUVoQyxNQUFNLFlBQVksR0FBRyxHQUFHLEVBQUUsQ0FBQyxDQUFDO0lBQzFCLEtBQUssRUFBRSxpQkFBaUI7SUFDeEIsTUFBTSxFQUFFLFlBQXFCO0lBQzdCLFFBQVEsRUFBRSxFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUU7Q0FDaEMsQ0FBQyxDQUFBO0FBRUYsSUFBQSxpQkFBUSxFQUFDLGtDQUFrQyxFQUFFLEdBQUcsRUFBRTtJQUNoRCxNQUFNLFVBQVUsR0FBRztRQUNqQjtZQUNFLElBQUksRUFBRSwwREFBMEQ7WUFDaEUsS0FBSyxFQUFFLEdBQUcsRUFBRTtnQkFDVixNQUFNLFNBQVMsR0FBRyxXQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsaUJBQWlCLENBQUM7b0JBQzFDLEVBQUUsRUFBRSxJQUFJO29CQUNSLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FDVCxPQUFPLENBQUMsT0FBTyxDQUFDO3dCQUNkLFlBQVksRUFBRSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUU7cUJBQzlCLENBQUM7aUJBQ0wsQ0FBQyxDQUFBO2dCQUNGLE9BQU87b0JBQ0wsU0FBUyxFQUFFLFNBQVM7b0JBQ3BCLFdBQVcsRUFBRSxzREFBc0Q7aUJBQ3BFLENBQUE7WUFDSCxDQUFDO1NBQ0Y7UUFDRDtZQUNFLElBQUksRUFBRSxnQ0FBZ0M7WUFDdEMsS0FBSyxFQUFFLEdBQUcsRUFBRTtnQkFDVixNQUFNLFNBQVMsR0FBRyxXQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsaUJBQWlCLENBQUM7b0JBQzFDLEVBQUUsRUFBRSxLQUFLO29CQUNULE1BQU0sRUFBRSxHQUFHO29CQUNYLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQztpQkFDdkMsQ0FBQyxDQUFBO2dCQUNGLE9BQU87b0JBQ0wsU0FBUyxFQUFFLFNBQVM7b0JBQ3BCLGFBQWEsRUFBRSxVQUFVO2lCQUMxQixDQUFBO1lBQ0gsQ0FBQztTQUNGO0tBQ0YsQ0FBQTtJQUVELFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFO1FBQ3JDLElBQUEsV0FBRSxFQUFDLElBQUksRUFBRSxLQUFLLElBQUksRUFBRTtZQUNsQixNQUFNLE1BQU0sR0FBRyxLQUFLLEVBQUUsQ0FBQTtZQUN0QixNQUFNLEVBQUUsU0FBUyxFQUFFLEdBQUcsTUFBTSxDQUFBO1lBRTVCLElBQUksZUFBZSxJQUFJLE1BQU0sRUFBRSxDQUFDO2dCQUM5QixNQUFNLElBQUEsZUFBTSxFQUNWLElBQUEsZ0RBQXlCLEVBQUMsWUFBWSxFQUFFLEVBQUU7b0JBQ3hDLFNBQVM7b0JBQ1QsT0FBTyxFQUFFLG9CQUFvQjtpQkFDOUIsQ0FBQyxDQUNILENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQztvQkFDdEIsT0FBTyxFQUFFLGVBQU0sQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQztpQkFDckQsQ0FBQyxDQUFBO2dCQUNGLE9BQU07WUFDUixDQUFDO1lBRUQsTUFBTSxNQUFNLEdBQUcsTUFBTSxJQUFBLGdEQUF5QixFQUFDLFlBQVksRUFBRSxFQUFFO2dCQUM3RCxTQUFTO2dCQUNULE9BQU8sRUFBRSxvQkFBb0I7YUFDOUIsQ0FBQyxDQUFBO1lBRUYsSUFBQSxlQUFNLEVBQUMsTUFBTSxDQUFDLENBQUMsT0FBTyxDQUFDLEVBQUUsWUFBWSxFQUFFLEVBQUUsRUFBRSxFQUFFLE9BQU8sRUFBRSxFQUFFLENBQUMsQ0FBQTtZQUN6RCxJQUFBLGVBQU0sRUFBQyxTQUFTLENBQUMsQ0FBQyxvQkFBb0IsQ0FDcEMsTUFBTSxDQUFDLFdBQVcsRUFDbEIsZUFBTSxDQUFDLFFBQVEsRUFBRSxDQUNsQixDQUFBO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtJQUVGLElBQUEsV0FBRSxFQUFDLGtDQUFrQyxFQUFFLEtBQUssSUFBSSxFQUFFO1FBQ2hELE1BQU0sVUFBVSxHQUFHO1lBQ2pCLE9BQU8sRUFBRSxXQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsaUJBQWlCLENBQUM7Z0JBQ2pDLFlBQVksRUFBRSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUU7YUFDOUIsQ0FBQztTQUNILENBQUE7UUFFRCxNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUEsZ0RBQXlCLEVBQUMsWUFBWSxFQUFFLEVBQUU7WUFDN0QsTUFBTSxFQUFFLFVBQVU7U0FDbkIsQ0FBQyxDQUFBO1FBRUYsSUFBQSxlQUFNLEVBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUE7UUFDNUMsSUFBQSxlQUFNLEVBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQyxDQUFDLG9CQUFvQixDQUM3QyxvQ0FBb0MsRUFDcEMsZUFBTSxDQUFDLGdCQUFnQixDQUFDO1lBQ3RCLE1BQU0sRUFBRSxNQUFNO1NBQ2YsQ0FBQyxDQUNILENBQUE7SUFDSCxDQUFDLENBQUMsQ0FBQTtJQUVGLElBQUEsV0FBRSxFQUFDLHlDQUF5QyxFQUFFLEtBQUssSUFBSSxFQUFFO1FBQ3ZELE1BQU0sU0FBUyxHQUFHLFdBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxpQkFBaUIsQ0FBQztZQUMxQyxFQUFFLEVBQUUsSUFBSTtZQUNSLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FDVCxPQUFPLENBQUMsT0FBTyxDQUFDO2dCQUNkLFlBQVksRUFBRSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUU7YUFDOUIsQ0FBQztTQUNMLENBQUMsQ0FBQTtRQUVGLE1BQU0sSUFBQSxnREFBeUIsRUFBQyxZQUFZLEVBQUUsRUFBRTtZQUM5QyxTQUFTLEVBQUUsU0FBUztZQUNwQixPQUFPLEVBQUUsb0JBQW9CO1lBQzdCLGlCQUFpQixFQUFFLGNBQWM7U0FDbEMsQ0FBQyxDQUFBO1FBRUYsTUFBTSxDQUFDLEVBQUUsSUFBSSxDQUFDLEdBQUcsU0FBUyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUE7UUFDeEMsSUFBQSxlQUFNLEVBQUMsSUFBSSxFQUFFLE9BQU8sQ0FBQyxDQUFDLGFBQWEsQ0FBQztZQUNsQyx1QkFBdUIsRUFBRSxjQUFjO1NBQ3hDLENBQUMsQ0FBQTtJQUNKLENBQUMsQ0FBQyxDQUFBO0lBRUYsSUFBQSxXQUFFLEVBQUMsK0NBQStDLEVBQUUsS0FBSyxJQUFJLEVBQUU7UUFDN0QsTUFBTSxTQUFTLEdBQUcsV0FBRSxDQUFDLEVBQUUsRUFBRSxDQUFDLGlCQUFpQixDQUFDO1lBQzFDLEVBQUUsRUFBRSxJQUFJO1lBQ1IsSUFBSSxFQUFFLEdBQUcsRUFBRSxDQUNULE9BQU8sQ0FBQyxPQUFPLENBQUM7Z0JBQ2QsWUFBWSxFQUFFLEVBQUUsRUFBRSxFQUFFLE9BQU8sRUFBRTthQUM5QixDQUFDO1NBQ0wsQ0FBQyxDQUFBO1FBRUYsTUFBTSxrQkFBa0IsR0FBRyxJQUFBLHNEQUErQixFQUFDO1lBQ3pELFNBQVMsRUFBRSxTQUFTO1lBQ3BCLE9BQU8sRUFBRSxvQkFBb0I7U0FDOUIsQ0FBQyxDQUFBO1FBRUYsTUFBTSxNQUFNLEdBQUcsTUFBTSxrQkFBa0IsQ0FBQyxZQUFZLEVBQUUsQ0FBQyxDQUFBO1FBQ3ZELElBQUEsZUFBTSxFQUFDLE1BQU0sQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFBO0lBQzlDLENBQUMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFDLENBQUEifQ==