@webbycrown/webbycommerce 1.2.0 → 2.0.0

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 (162) hide show
  1. package/README.md +26 -3
  2. package/admin/app.js +3 -0
  3. package/admin/jsconfig.json +20 -0
  4. package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
  5. package/admin/src/components/CompareContent.jsx +300 -0
  6. package/admin/src/components/ConfigureContent.jsx +407 -0
  7. package/admin/src/components/Initializer.jsx +64 -0
  8. package/admin/src/components/LoginRegisterContent.jsx +280 -0
  9. package/admin/src/components/PluginIcon.jsx +6 -0
  10. package/admin/src/components/ShippingTypeContent.jsx +230 -0
  11. package/admin/src/components/SmtpContent.jsx +316 -0
  12. package/admin/src/components/WishlistContent.jsx +273 -0
  13. package/admin/src/index.js +81 -0
  14. package/admin/src/pages/ApiCollections.jsx +169 -0
  15. package/admin/src/pages/Configure.jsx +55 -0
  16. package/admin/src/pages/Settings.jsx +93 -0
  17. package/admin/src/pluginId.js +4 -0
  18. package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
  19. package/bin/setup.js +50 -3
  20. package/package.json +14 -13
  21. package/server/bootstrap.js +3 -0
  22. package/server/register.js +3 -0
  23. package/server/src/bootstrap.js +3826 -0
  24. package/server/src/components/content-block.json +37 -0
  25. package/server/src/components/shipping-zone-location.json +27 -0
  26. package/server/src/config/index.js +7 -0
  27. package/server/src/content-types/address/index.js +7 -0
  28. package/server/src/content-types/address/schema.json +74 -0
  29. package/server/src/content-types/cart/index.js +61 -0
  30. package/server/src/content-types/cart-item/index.js +79 -0
  31. package/server/src/content-types/compare.js +73 -0
  32. package/server/src/content-types/coupon/index.js +7 -0
  33. package/server/src/content-types/coupon/schema.json +67 -0
  34. package/server/src/content-types/index.js +42 -0
  35. package/server/src/content-types/order/index.js +7 -0
  36. package/server/src/content-types/order/schema.json +121 -0
  37. package/server/src/content-types/payment-transaction/index.js +7 -0
  38. package/server/src/content-types/payment-transaction/schema.json +73 -0
  39. package/server/src/content-types/product/index.js +7 -0
  40. package/server/src/content-types/product/schema.json +104 -0
  41. package/server/src/content-types/product-attribute/index.js +7 -0
  42. package/server/src/content-types/product-attribute/schema.json +80 -0
  43. package/server/src/content-types/product-attribute-value/index.js +7 -0
  44. package/server/src/content-types/product-attribute-value/schema.json +52 -0
  45. package/server/src/content-types/product-category/index.js +7 -0
  46. package/server/src/content-types/product-category/schema.json +54 -0
  47. package/server/src/content-types/product-tag/index.js +7 -0
  48. package/server/src/content-types/product-tag/schema.json +38 -0
  49. package/server/src/content-types/product-variation/index.js +7 -0
  50. package/server/src/content-types/product-variation/schema.json +74 -0
  51. package/server/src/content-types/shipping-method/index.js +7 -0
  52. package/server/src/content-types/shipping-method/schema.json +91 -0
  53. package/server/src/content-types/shipping-rate/index.js +7 -0
  54. package/server/src/content-types/shipping-rate/schema.json +73 -0
  55. package/server/src/content-types/shipping-rule/index.js +7 -0
  56. package/server/src/content-types/shipping-rule/schema.json +84 -0
  57. package/server/src/content-types/shipping-zone/index.js +7 -0
  58. package/server/src/content-types/shipping-zone/schema.json +57 -0
  59. package/server/src/content-types/wishlist.js +66 -0
  60. package/server/src/controllers/address.js +374 -0
  61. package/server/src/controllers/auth.js +1409 -0
  62. package/server/src/controllers/cart.js +337 -0
  63. package/server/src/controllers/category.js +388 -0
  64. package/server/src/controllers/compare.js +246 -0
  65. package/server/src/controllers/controller.js +168 -0
  66. package/server/src/controllers/ecommerce.js +20 -0
  67. package/server/src/controllers/index.js +34 -0
  68. package/server/src/controllers/order.js +1100 -0
  69. package/server/src/controllers/payment.js +243 -0
  70. package/server/src/controllers/product.js +1006 -0
  71. package/server/src/controllers/productTag.js +370 -0
  72. package/server/src/controllers/productVariation.js +181 -0
  73. package/server/src/controllers/shipping.js +1046 -0
  74. package/server/src/controllers/wishlist.js +332 -0
  75. package/server/src/destroy.js +6 -0
  76. package/server/src/index.js +26 -0
  77. package/server/src/middlewares/index.js +4 -0
  78. package/server/src/policies/index.js +4 -0
  79. package/server/src/register.js +67 -0
  80. package/server/src/routes/index.js +1130 -0
  81. package/server/src/services/cart.js +531 -0
  82. package/server/src/services/compare.js +300 -0
  83. package/server/src/services/index.js +16 -0
  84. package/server/src/services/service.js +19 -0
  85. package/server/src/services/shipping.js +513 -0
  86. package/server/src/services/wishlist.js +238 -0
  87. package/server/src/utils/check-ecommerce-permission.js +204 -0
  88. package/server/src/utils/extend-user-schema.js +161 -0
  89. package/server/src/utils/seed-data.js +639 -0
  90. package/server/src/utils/send-email.js +98 -0
  91. package/strapi-server.js +1 -6
  92. package/dist/_chunks/Settings-DZXAkI24.js +0 -31539
  93. package/dist/_chunks/Settings-yLx-YvVy.mjs +0 -31520
  94. package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
  95. package/dist/_chunks/index-CXGrFKp6.mjs +0 -128
  96. package/dist/_chunks/index-DgocXUgC.js +0 -127
  97. package/dist/admin/index.js +0 -3
  98. package/dist/admin/index.mjs +0 -4
  99. package/dist/robots.txt +0 -3
  100. package/dist/server/index.js +0 -27078
  101. package/dist/uploads/.gitkeep +0 -0
  102. package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
  103. package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  104. package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
  105. package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
  106. package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
  107. package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
  108. package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
  109. package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
  110. package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
  111. package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  112. package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  113. package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
  114. package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
  115. package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  116. package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  117. package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
  118. package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
  119. package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  120. package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  121. package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  122. package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  123. package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  124. package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
  125. package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  126. package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  127. package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  128. package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
  129. package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
  130. package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  131. package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  132. package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  133. package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  134. package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  135. package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
  136. package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  137. package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
  138. package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
  139. package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
  140. package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
  141. package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  142. package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
  143. package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  144. package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
  145. package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
  146. package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
  147. package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
  148. package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
  149. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  150. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  151. package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  152. package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  153. package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  154. package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  155. package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  156. package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  157. package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
  158. package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  159. package/dist/uploads/webby-commerce.png +0 -0
  160. package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
  161. package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  162. /package/{dist → server/src}/data/demo-data.json +0 -0
@@ -0,0 +1,300 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ Layout,
4
+ HeaderLayout,
5
+ ContentLayout,
6
+ ActionLayout,
7
+ } from '@strapi/design-system/Layout';
8
+ import {
9
+ Box,
10
+ Typography,
11
+ Button,
12
+ Table,
13
+ Thead,
14
+ Tbody,
15
+ Tr,
16
+ Td,
17
+ Th,
18
+ Avatar,
19
+ Badge,
20
+ Flex,
21
+ IconButton,
22
+ } from '@strapi/design-system';
23
+ import {
24
+ Eye,
25
+ Pencil,
26
+ Trash,
27
+ Plus,
28
+ } from '@strapi/icons';
29
+
30
+ // Alternative imports if the above don't work:
31
+ // import { EyeIcon, PencilIcon, TrashIcon, PlusIcon } from '@strapi/icons';
32
+ import { useIntl } from 'react-intl';
33
+ import { useFetchClient } from '@strapi/admin/strapi-admin';
34
+ import { useNotification } from '@strapi/admin/strapi-admin';
35
+
36
+ const CompareContent = () => {
37
+ const { formatMessage } = useIntl();
38
+ const { get, del } = useFetchClient();
39
+ const { toggleNotification } = useNotification();
40
+
41
+ const [compares, setCompares] = useState([]);
42
+ const [loading, setLoading] = useState(true);
43
+ const [selectedCompare, setSelectedCompare] = useState(null);
44
+
45
+ useEffect(() => {
46
+ fetchCompares();
47
+ }, []);
48
+
49
+ const fetchCompares = async () => {
50
+ try {
51
+ setLoading(true);
52
+ const response = await get('/webbycommerce/compares');
53
+ setCompares(response.data.data || []);
54
+ } catch (error) {
55
+ console.error('Error fetching compares:', error);
56
+ toggleNotification({
57
+ type: 'warning',
58
+ message: 'Failed to fetch compare lists',
59
+ });
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ };
64
+
65
+ const handleDeleteCompare = async (compareId) => {
66
+ if (!confirm('Are you sure you want to delete this compare list?')) {
67
+ return;
68
+ }
69
+
70
+ try {
71
+ await del(`/webbycommerce/compares/${compareId}`);
72
+ setCompares(compares.filter(c => c.id !== compareId));
73
+ toggleNotification({
74
+ type: 'success',
75
+ message: 'Compare list deleted successfully',
76
+ });
77
+ } catch (error) {
78
+ console.error('Error deleting compare:', error);
79
+ toggleNotification({
80
+ type: 'warning',
81
+ message: 'Failed to delete compare list',
82
+ });
83
+ }
84
+ };
85
+
86
+ const formatDate = (dateString) => {
87
+ return new Date(dateString).toLocaleDateString();
88
+ };
89
+
90
+ return (
91
+ <Layout>
92
+ <HeaderLayout
93
+ title="Compare Management"
94
+ subtitle="Manage user compare lists and product comparisons"
95
+ as="h2"
96
+ />
97
+ <ActionLayout
98
+ startActions={
99
+ <Button
100
+ onClick={fetchCompares}
101
+ loading={loading}
102
+ variant="secondary"
103
+ >
104
+ Refresh
105
+ </Button>
106
+ }
107
+ />
108
+ <ContentLayout>
109
+ <Box
110
+ background="neutral0"
111
+ hasRadius
112
+ shadow="filterShadow"
113
+ padding={6}
114
+ >
115
+ {loading ? (
116
+ <Box padding={4}>
117
+ <Typography>Loading compare lists...</Typography>
118
+ </Box>
119
+ ) : compares.length === 0 ? (
120
+ <Box padding={4}>
121
+ <Typography>No compare lists found.</Typography>
122
+ </Box>
123
+ ) : (
124
+ <Table colCount={5} rowCount={compares.length}>
125
+ <Thead>
126
+ <Tr>
127
+ <Th>
128
+ <Typography variant="sigma">User</Typography>
129
+ </Th>
130
+ <Th>
131
+ <Typography variant="sigma">Products</Typography>
132
+ </Th>
133
+ <Th>
134
+ <Typography variant="sigma">Category</Typography>
135
+ </Th>
136
+ <Th>
137
+ <Typography variant="sigma">Public</Typography>
138
+ </Th>
139
+ <Th>
140
+ <Typography variant="sigma">Actions</Typography>
141
+ </Th>
142
+ </Tr>
143
+ </Thead>
144
+ <Tbody>
145
+ {compares.map((compare) => (
146
+ <Tr key={compare.id}>
147
+ <Td>
148
+ <Flex>
149
+ <Avatar
150
+ src={null}
151
+ alt={compare.userEmail}
152
+ initials={compare.userEmail?.charAt(0).toUpperCase()}
153
+ />
154
+ <Box marginLeft={2}>
155
+ <Typography>{compare.userEmail}</Typography>
156
+ <Typography variant="pi" textColor="neutral600">
157
+ ID: {compare.userId}
158
+ </Typography>
159
+ </Box>
160
+ </Flex>
161
+ </Td>
162
+ <Td>
163
+ <Badge>{compare.products?.length || 0}/4 products</Badge>
164
+ </Td>
165
+ <Td>
166
+ <Typography variant="pi">
167
+ {compare.category?.name || 'Mixed'}
168
+ </Typography>
169
+ </Td>
170
+ <Td>
171
+ <Badge color={compare.isPublic ? 'success' : 'neutral'}>
172
+ {compare.isPublic ? 'Public' : 'Private'}
173
+ </Badge>
174
+ </Td>
175
+ <Td>
176
+ <Flex gap={1}>
177
+ <IconButton
178
+ onClick={() => setSelectedCompare(compare)}
179
+ label="View compare list"
180
+ icon={<Eye />}
181
+ />
182
+ <IconButton
183
+ onClick={() => handleDeleteCompare(compare.id)}
184
+ label="Delete compare list"
185
+ icon={<Trash />}
186
+ />
187
+ </Flex>
188
+ </Td>
189
+ </Tr>
190
+ ))}
191
+ </Tbody>
192
+ </Table>
193
+ )}
194
+ </Box>
195
+
196
+ {/* Compare Detail Modal/View */}
197
+ {selectedCompare && (
198
+ <Box
199
+ background="neutral0"
200
+ hasRadius
201
+ shadow="filterShadow"
202
+ padding={6}
203
+ marginTop={4}
204
+ >
205
+ <Typography variant="beta" marginBottom={4}>
206
+ Compare List Details - {selectedCompare.userEmail}
207
+ </Typography>
208
+
209
+ {selectedCompare.name && (
210
+ <Typography variant="epsilon" marginBottom={2}>
211
+ Name: {selectedCompare.name}
212
+ </Typography>
213
+ )}
214
+
215
+ {selectedCompare.notes && (
216
+ <Typography variant="pi" marginBottom={4}>
217
+ Notes: {selectedCompare.notes}
218
+ </Typography>
219
+ )}
220
+
221
+ <Typography variant="delta" marginBottom={3}>
222
+ Products ({selectedCompare.products?.length || 0}/4):
223
+ </Typography>
224
+
225
+ {selectedCompare.products?.length > 0 ? (
226
+ <Flex gap={4} wrap="wrap">
227
+ {selectedCompare.products.map((product) => (
228
+ <Box key={product.id} flex="1" minWidth="300px">
229
+ <Box
230
+ padding={4}
231
+ background="neutral100"
232
+ hasRadius
233
+ borderColor="neutral200"
234
+ >
235
+ <Flex alignItems="center" marginBottom={3}>
236
+ {product.images?.[0] && (
237
+ <Box marginRight={3}>
238
+ <img
239
+ src={product.images[0].url}
240
+ alt={product.name}
241
+ style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
242
+ />
243
+ </Box>
244
+ )}
245
+ <Box flex={1}>
246
+ <Typography variant="omega" fontWeight="bold">
247
+ {product.name}
248
+ </Typography>
249
+ <Typography variant="pi" textColor="neutral600">
250
+ ${product.price || 'N/A'}
251
+ </Typography>
252
+ </Box>
253
+ <Badge color={product.stockQuantity > 0 ? 'success' : 'danger'}>
254
+ {product.stockQuantity > 0 ? 'In Stock' : 'Out of Stock'}
255
+ </Badge>
256
+ </Flex>
257
+
258
+ {product.specifications && (
259
+ <Box>
260
+ <Typography variant="pi" fontWeight="bold" marginBottom={2}>
261
+ Specifications:
262
+ </Typography>
263
+ {Object.entries(product.specifications).map(([key, value]) => (
264
+ <Flex key={key} justifyContent="space-between" marginBottom={1}>
265
+ <Typography variant="pi" textColor="neutral600">
266
+ {key}:
267
+ </Typography>
268
+ <Typography variant="pi">
269
+ {value || 'N/A'}
270
+ </Typography>
271
+ </Flex>
272
+ ))}
273
+ </Box>
274
+ )}
275
+ </Box>
276
+ </Box>
277
+ ))}
278
+ </Flex>
279
+ ) : (
280
+ <Typography variant="pi" textColor="neutral600">
281
+ No products in this compare list
282
+ </Typography>
283
+ )}
284
+
285
+ <Flex marginTop={4} justifyContent="flex-end">
286
+ <Button
287
+ onClick={() => setSelectedCompare(null)}
288
+ variant="tertiary"
289
+ >
290
+ Close
291
+ </Button>
292
+ </Flex>
293
+ </Box>
294
+ )}
295
+ </ContentLayout>
296
+ </Layout>
297
+ );
298
+ };
299
+
300
+ export default CompareContent;
@@ -0,0 +1,407 @@
1
+ 'use strict';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useIntl } from 'react-intl';
5
+ import { Box, Flex, Typography, TextInput, Button } from '@strapi/design-system';
6
+ import { Trash } from '@strapi/icons';
7
+ import { useFetchClient } from '@strapi/admin/strapi-admin';
8
+
9
+ import { PLUGIN_ID } from '../pluginId';
10
+
11
+ const emptyOriginRow = () => ({
12
+ id: Math.random().toString(36).slice(2),
13
+ value: '',
14
+ });
15
+
16
+ const ConfigureContent = () => {
17
+ const { formatMessage } = useIntl();
18
+ const fetchClient = useFetchClient();
19
+
20
+ const [rows, setRows] = useState([emptyOriginRow()]);
21
+ const [routePrefix, setRoutePrefix] = useState('webbycommerce');
22
+ const [isLoading, setIsLoading] = useState(false);
23
+ const [isSaving, setIsSaving] = useState(false);
24
+ const [isSeeding, setIsSeeding] = useState(false);
25
+ const [feedback, setFeedback] = useState(null);
26
+
27
+ const title = formatMessage({
28
+ id: `${PLUGIN_ID}.settings.configure.title`,
29
+ defaultMessage: 'Configure',
30
+ });
31
+
32
+ const description = formatMessage({
33
+ id: `${PLUGIN_ID}.settings.configure.description`,
34
+ defaultMessage:
35
+ 'Global settings for the Strapi Advanced Ecommerce plugin. Use this page to control ecommerce-wide behavior.',
36
+ });
37
+
38
+ useEffect(() => {
39
+ let isMounted = true;
40
+ const loadSettings = async () => {
41
+ try {
42
+ setIsLoading(true);
43
+ const { data } = await fetchClient.get(`/webbycommerce/settings`);
44
+ const origins = Array.isArray(data?.allowedOrigins) ? data.allowedOrigins : [];
45
+ const prefix = data?.routePrefix || 'webbycommerce';
46
+ if (!isMounted) return;
47
+
48
+ setRoutePrefix(prefix);
49
+ if (origins.length) {
50
+ setRows(
51
+ origins.map((value) => ({
52
+ id: Math.random().toString(36).slice(2),
53
+ value,
54
+ }))
55
+ );
56
+ } else {
57
+ setRows([emptyOriginRow()]);
58
+ }
59
+ } catch (error) {
60
+ if (isMounted) {
61
+ setFeedback({
62
+ type: 'error',
63
+ message: formatMessage({
64
+ id: `${PLUGIN_ID}.settings.configure.load.error`,
65
+ defaultMessage: 'Failed to load settings.',
66
+ }),
67
+ });
68
+ }
69
+ } finally {
70
+ if (isMounted) {
71
+ setIsLoading(false);
72
+ }
73
+ }
74
+ };
75
+
76
+ loadSettings();
77
+
78
+ return () => {
79
+ isMounted = false;
80
+ };
81
+ }, [fetchClient, formatMessage]);
82
+
83
+ const updateRowValue = (id, value) => {
84
+ setRows((current) =>
85
+ current.map((row) => (row.id === id ? { ...row, value } : row))
86
+ );
87
+ };
88
+
89
+ const addRow = () => {
90
+ setRows((current) => [...current, emptyOriginRow()]);
91
+ };
92
+
93
+ const removeRow = (id) => {
94
+ setRows((current) => {
95
+ const next = current.filter((row) => row.id !== id);
96
+ return next.length ? next : [emptyOriginRow()];
97
+ });
98
+ };
99
+
100
+ const handleSave = async () => {
101
+ try {
102
+ setIsSaving(true);
103
+ setFeedback(null);
104
+
105
+ const allowedOrigins = rows
106
+ .map((row) => (typeof row.value === 'string' ? row.value.trim() : ''))
107
+ .filter((value) => value.length > 0);
108
+
109
+ // Sanitize route prefix
110
+ const sanitizedPrefix = (routePrefix || 'webbycommerce')
111
+ .trim()
112
+ .replace(/^\/+|\/+$/g, '') // Remove leading/trailing slashes
113
+ .replace(/\/+/g, '/') // Replace multiple slashes with single
114
+ .replace(/[^a-zA-Z0-9\/_-]/g, '') // Remove invalid characters
115
+ || 'webbycommerce';
116
+
117
+ const { data } = await fetchClient.put(`/webbycommerce/settings`, {
118
+ allowedOrigins,
119
+ routePrefix: sanitizedPrefix,
120
+ });
121
+
122
+ const persisted = Array.isArray(data?.allowedOrigins) ? data.allowedOrigins : [];
123
+ setRows(
124
+ (persisted.length ? persisted : ['']).map((value) => ({
125
+ id: Math.random().toString(36).slice(2),
126
+ value,
127
+ }))
128
+ );
129
+
130
+ setFeedback({
131
+ type: 'success',
132
+ message: formatMessage({
133
+ id: `${PLUGIN_ID}.settings.configure.save.success`,
134
+ defaultMessage: 'Settings updated successfully.',
135
+ }),
136
+ });
137
+ } catch (error) {
138
+ setFeedback({
139
+ type: 'error',
140
+ message: formatMessage({
141
+ id: `${PLUGIN_ID}.settings.configure.save.error`,
142
+ defaultMessage: 'Failed to save settings.',
143
+ }),
144
+ });
145
+ } finally {
146
+ setIsSaving(false);
147
+ }
148
+ };
149
+
150
+ const handleSeedDemo = async () => {
151
+ if (!window.confirm(formatMessage({
152
+ id: `${PLUGIN_ID}.settings.configure.demo.confirm`,
153
+ defaultMessage: 'Are you sure you want to seed demo data? This will overwrite existing data with the same slugs.'
154
+ }))) {
155
+ return;
156
+ }
157
+
158
+ try {
159
+ setIsSeeding(true);
160
+ setFeedback(null);
161
+
162
+ const { data } = await fetchClient.post(`/webbycommerce/seed-demo`);
163
+
164
+ if (data.success) {
165
+ setFeedback({
166
+ type: 'success',
167
+ message: formatMessage({
168
+ id: `${PLUGIN_ID}.settings.configure.demo.success`,
169
+ defaultMessage: 'Demo data seeded successfully!',
170
+ }),
171
+ });
172
+ } else {
173
+ throw new Error(data.message);
174
+ }
175
+ } catch (error) {
176
+ setFeedback({
177
+ type: 'error',
178
+ message: formatMessage({
179
+ id: `${PLUGIN_ID}.settings.configure.demo.error`,
180
+ defaultMessage: 'Failed to seed demo data.',
181
+ }) + (error.message ? `: ${error.message}` : ''),
182
+ });
183
+ } finally {
184
+ setIsSeeding(false);
185
+ }
186
+ };
187
+
188
+ return (
189
+ <Box paddingTop={6}>
190
+ <Typography variant="beta" textColor="neutral800">
191
+ {title}
192
+ </Typography>
193
+
194
+ <Box marginTop={2}>
195
+ <Typography variant="pi" textColor="neutral600">
196
+ {description}
197
+ </Typography>
198
+ </Box>
199
+
200
+ <Box marginTop={6}>
201
+ <Box
202
+ background="neutral0"
203
+ hasRadius
204
+ shadow="filterShadow"
205
+ padding={4}
206
+ style={{ maxWidth: '640px' }}
207
+ >
208
+ <Typography variant="delta" textColor="neutral800">
209
+ {formatMessage({
210
+ id: `${PLUGIN_ID}.settings.configure.routePrefix.title`,
211
+ defaultMessage: 'API Route Prefix',
212
+ })}
213
+ </Typography>
214
+ <Box marginTop={1}>
215
+ <Typography variant="pi" textColor="neutral600">
216
+ {formatMessage({
217
+ id: `${PLUGIN_ID}.settings.configure.routePrefix.description`,
218
+ defaultMessage:
219
+ 'Customize the API route prefix. All ecommerce API endpoints will use this prefix. Default: webbycommerce',
220
+ })}
221
+ </Typography>
222
+ </Box>
223
+ <Box marginTop={4}>
224
+ <TextInput
225
+ name="routePrefix"
226
+ label={formatMessage({
227
+ id: `${PLUGIN_ID}.settings.configure.routePrefix.label`,
228
+ defaultMessage: 'Route prefix',
229
+ })}
230
+ placeholder={formatMessage({
231
+ id: `${PLUGIN_ID}.settings.configure.routePrefix.placeholder`,
232
+ defaultMessage: 'e.g. ecommerce, v1, api/ecommerce',
233
+ })}
234
+ value={routePrefix}
235
+ onChange={(event) => setRoutePrefix(event.target.value)}
236
+ disabled={isLoading || isSaving}
237
+ hint={formatMessage({
238
+ id: `${PLUGIN_ID}.settings.configure.routePrefix.hint`,
239
+ defaultMessage: 'API endpoints will be available at /api/{prefix}/...',
240
+ })}
241
+ />
242
+ </Box>
243
+ </Box>
244
+
245
+ <Box marginTop={6}>
246
+ <Box
247
+ background="neutral0"
248
+ hasRadius
249
+ shadow="filterShadow"
250
+ padding={4}
251
+ style={{ maxWidth: '640px' }}
252
+ >
253
+ <Typography variant="delta" textColor="neutral800">
254
+ {formatMessage({
255
+ id: `${PLUGIN_ID}.settings.configure.origins.title`,
256
+ defaultMessage: 'Allowed frontend domains',
257
+ })}
258
+ </Typography>
259
+ <Box marginTop={1}>
260
+ <Typography variant="pi" textColor="neutral600">
261
+ {formatMessage({
262
+ id: `${PLUGIN_ID}.settings.configure.origins.description`,
263
+ defaultMessage:
264
+ 'Only requests coming from these domains will be allowed to use the ecommerce facility. Leave empty to allow all domains.',
265
+ })}
266
+ </Typography>
267
+ </Box>
268
+
269
+ <Box marginTop={4}>
270
+ {rows.map((row, index) => (
271
+ <Box
272
+ key={row.id}
273
+ marginTop={index === 0 ? 2 : 4}
274
+ padding={4}
275
+ hasRadius
276
+ background="neutral100"
277
+ style={{ border: '1px solid #dcdce4' }}
278
+ >
279
+ <Flex
280
+ justifyContent="space-between"
281
+ alignItems="flex-start"
282
+ gap={4}
283
+ wrap="wrap"
284
+ >
285
+ <Box style={{ flex: 1, minWidth: 0 }}>
286
+ <Typography variant="epsilon" textColor="neutral800">
287
+ {formatMessage(
288
+ {
289
+ id: `${PLUGIN_ID}.settings.configure.origins.entryTitle`,
290
+ defaultMessage: 'Domain #{index}',
291
+ },
292
+ { index: index + 1 }
293
+ )}
294
+ </Typography>
295
+ <Box marginTop={2}>
296
+ <TextInput
297
+ name={`origin-${row.id}`}
298
+ label={formatMessage({
299
+ id: `${PLUGIN_ID}.settings.configure.origins.label`,
300
+ defaultMessage: 'Frontend domain',
301
+ })}
302
+ placeholder={formatMessage({
303
+ id: `${PLUGIN_ID}.settings.configure.origins.placeholder`,
304
+ defaultMessage: 'e.g. https://shop.example.com',
305
+ })}
306
+ value={row.value}
307
+ onChange={(event) => updateRowValue(row.id, event.target.value)}
308
+ disabled={isLoading || isSaving}
309
+ />
310
+ </Box>
311
+ </Box>
312
+ <Button
313
+ variant="tertiary"
314
+ startIcon={<Trash />}
315
+ onClick={() => removeRow(row.id)}
316
+ disabled={isLoading || isSaving}
317
+ >
318
+ {formatMessage({
319
+ id: `${PLUGIN_ID}.settings.configure.origins.remove`,
320
+ defaultMessage: 'Remove domain',
321
+ })}
322
+ </Button>
323
+ </Flex>
324
+ </Box>
325
+ ))}
326
+
327
+ <Box>
328
+ <Button
329
+ marginTop={2}
330
+ variant="tertiary"
331
+ onClick={addRow}
332
+ disabled={isLoading || isSaving}
333
+ >
334
+ {formatMessage({
335
+ id: `${PLUGIN_ID}.settings.configure.origins.add`,
336
+ defaultMessage: 'Add domain',
337
+ })}
338
+ </Button>
339
+ </Box>
340
+ </Box>
341
+ </Box>
342
+ </Box>
343
+
344
+ <Box marginTop={6}>
345
+ <Box
346
+ background="neutral0"
347
+ hasRadius
348
+ shadow="filterShadow"
349
+ padding={4}
350
+ style={{ maxWidth: '640px' }}
351
+ >
352
+ <Typography variant="delta" textColor="neutral800">
353
+ {formatMessage({
354
+ id: `${PLUGIN_ID}.settings.configure.demo.title`,
355
+ defaultMessage: 'Demo Data',
356
+ })}
357
+ </Typography>
358
+ <Box marginTop={1}>
359
+ <Typography variant="pi" textColor="neutral600">
360
+ {formatMessage({
361
+ id: `${PLUGIN_ID}.settings.configure.demo.description`,
362
+ defaultMessage:
363
+ 'Import sample products, categories, tags, and shipping settings to test the plugin. This will not overwrite existing data with the same slugs.',
364
+ })}
365
+ </Typography>
366
+ </Box>
367
+ <Box marginTop={4}>
368
+ <Button
369
+ variant="secondary"
370
+ onClick={handleSeedDemo}
371
+ loading={isSeeding}
372
+ disabled={isLoading || isSaving || isSeeding}
373
+ >
374
+ {formatMessage({
375
+ id: `${PLUGIN_ID}.settings.configure.demo.button`,
376
+ defaultMessage: 'Seed Demo Data',
377
+ })}
378
+ </Button>
379
+ </Box>
380
+ </Box>
381
+ </Box>
382
+
383
+ <Box marginTop={6}>
384
+ <Flex gap={3} alignItems="center">
385
+ <Button onClick={handleSave} loading={isSaving} disabled={isLoading || isSaving}>
386
+ {formatMessage({
387
+ id: `${PLUGIN_ID}.settings.configure.save`,
388
+ defaultMessage: 'Save settings',
389
+ })}
390
+ </Button>
391
+ {feedback && (
392
+ <Typography
393
+ variant="pi"
394
+ textColor={feedback.type === 'error' ? 'danger600' : 'success600'}
395
+ >
396
+ {feedback.message}
397
+ </Typography>
398
+ )}
399
+ </Flex>
400
+ </Box>
401
+ </Box>
402
+ </Box>
403
+ );
404
+ };
405
+
406
+ export default ConfigureContent;
407
+