@plusscommunities/pluss-feature-builder-web-d 1.0.2-beta.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 (117) hide show
  1. package/.babelrc +4 -0
  2. package/dist/index.cjs.js +7792 -0
  3. package/package.json +54 -0
  4. package/rollup.config.js +68 -0
  5. package/src/actions/featureBuilderStringsActions.js +88 -0
  6. package/src/actions/featureDefinitionsIndex.js +258 -0
  7. package/src/actions/formActions.js +311 -0
  8. package/src/actions/index.js +12 -0
  9. package/src/actions/listingActions.js +350 -0
  10. package/src/actions/wizardActions.js +240 -0
  11. package/src/components/ActivityCardExample.jsx +86 -0
  12. package/src/components/ActivityCardExample.module.css +130 -0
  13. package/src/components/BackgroundLoader.jsx +33 -0
  14. package/src/components/BackgroundLoader.module.css +46 -0
  15. package/src/components/BaseFieldConfig.jsx +305 -0
  16. package/src/components/BaseFieldConfig.module.css +42 -0
  17. package/src/components/CenteredContainer.jsx +29 -0
  18. package/src/components/CenteredContainer.module.css +171 -0
  19. package/src/components/DeleteConfirmationPopup.jsx +95 -0
  20. package/src/components/DeleteConfirmationPopup.module.css +12 -0
  21. package/src/components/ErrorBoundary.jsx +134 -0
  22. package/src/components/ErrorBoundary.module.css +77 -0
  23. package/src/components/ErrorMessage.jsx +85 -0
  24. package/src/components/ErrorMessage.module.css +116 -0
  25. package/src/components/ExampleDisplay.jsx +26 -0
  26. package/src/components/ExampleDisplay.module.css +3 -0
  27. package/src/components/FeatureBuilderSidebar.jsx +84 -0
  28. package/src/components/FeatureBuilderSuccessPopup.jsx +55 -0
  29. package/src/components/FeatureBuilderSuccessPopup.module.css +43 -0
  30. package/src/components/FeatureBuilderWelcomePopup.jsx +51 -0
  31. package/src/components/FeatureBuilderWelcomePopup.module.css +21 -0
  32. package/src/components/FeatureListingCard.jsx +104 -0
  33. package/src/components/FeatureListingCard.module.css +62 -0
  34. package/src/components/Fields.jsx +460 -0
  35. package/src/components/Fields.module.css +159 -0
  36. package/src/components/IconLoader.jsx +153 -0
  37. package/src/components/IconLoader.module.css +92 -0
  38. package/src/components/IconSelector.jsx +112 -0
  39. package/src/components/IconSelector.module.css +197 -0
  40. package/src/components/ListingEditor.jsx +406 -0
  41. package/src/components/ListingEditor.module.css +14 -0
  42. package/src/components/ListingSuccessPopup.jsx +52 -0
  43. package/src/components/LoadingScreen.jsx +54 -0
  44. package/src/components/LoadingScreen.module.css +103 -0
  45. package/src/components/LoadingState.jsx +40 -0
  46. package/src/components/LoadingState.module.css +18 -0
  47. package/src/components/PreviewFull.js +24 -0
  48. package/src/components/PreviewFull.module.css +11 -0
  49. package/src/components/PreviewGrid.js +14 -0
  50. package/src/components/PreviewWidget.js +27 -0
  51. package/src/components/PreviewWidget.module.css +15 -0
  52. package/src/components/SidebarLayout.jsx +292 -0
  53. package/src/components/SidebarLayout.module.css +145 -0
  54. package/src/components/SkeletonLoader.jsx +128 -0
  55. package/src/components/SkeletonLoader.module.css +295 -0
  56. package/src/components/SortButtonGroup.jsx +34 -0
  57. package/src/components/SortButtonGroup.module.css +51 -0
  58. package/src/components/ToastContainer.jsx +98 -0
  59. package/src/components/ToastContainer.module.css +156 -0
  60. package/src/components/ToggleSwitch.js +40 -0
  61. package/src/components/ToggleSwitch.module.css +48 -0
  62. package/src/components/TwoColumnInput.jsx +29 -0
  63. package/src/components/TwoColumnInput.module.css +32 -0
  64. package/src/components/ViewFull.js +139 -0
  65. package/src/components/ViewFull.module.css +71 -0
  66. package/src/components/ViewWidget.js +62 -0
  67. package/src/components/ViewWidget.module.css +28 -0
  68. package/src/components/iconCategories.js +135 -0
  69. package/src/components/iconImports.js +409 -0
  70. package/src/components/index.js +61 -0
  71. package/src/components/listing/FileListItem.jsx +86 -0
  72. package/src/components/listing/GalleryDisplay.jsx +331 -0
  73. package/src/components/listing/GalleryDisplay.module.css +309 -0
  74. package/src/components/listing/ListingCTAInput.jsx +82 -0
  75. package/src/components/listing/ListingDescriptionInput.jsx +73 -0
  76. package/src/components/listing/ListingField.jsx +101 -0
  77. package/src/components/listing/ListingField.module.css +106 -0
  78. package/src/components/listing/ListingFileInput.jsx +255 -0
  79. package/src/components/listing/ListingFileInput.module.css +192 -0
  80. package/src/components/listing/ListingForm.jsx +90 -0
  81. package/src/components/listing/ListingForm.module.css +38 -0
  82. package/src/components/listing/ListingGalleryInput.jsx +236 -0
  83. package/src/components/listing/ListingGalleryInput.module.css +131 -0
  84. package/src/components/listing/ListingImageInput.jsx +153 -0
  85. package/src/components/listing/ListingTextInput.jsx +72 -0
  86. package/src/feature.config.js +130 -0
  87. package/src/helper/index.js +135 -0
  88. package/src/hooks/useFeatureDefinitionLoader.js +62 -0
  89. package/src/images/full.png +0 -0
  90. package/src/images/fullNoTitle.png +0 -0
  91. package/src/images/previewWidget.png +0 -0
  92. package/src/images/widget.png +0 -0
  93. package/src/index.js +38 -0
  94. package/src/pages/CreateListingPage.jsx +49 -0
  95. package/src/pages/EditListingPage.jsx +58 -0
  96. package/src/reducers/featureBuilderReducer.js +744 -0
  97. package/src/screens/CreateListing.module.css +45 -0
  98. package/src/screens/Form.module.css +734 -0
  99. package/src/screens/FormFieldsStep.jsx +689 -0
  100. package/src/screens/FormLayoutStep.jsx +445 -0
  101. package/src/screens/FormOverviewStep.jsx +396 -0
  102. package/src/screens/ListingScreen.jsx +478 -0
  103. package/src/screens/ListingScreen.module.css +333 -0
  104. package/src/selectors/featureBuilderSelectors.js +529 -0
  105. package/src/types/index.js +91 -0
  106. package/src/utils/textUtils.js +89 -0
  107. package/src/validators/galleryValidators.js +345 -0
  108. package/src/values.config.a.js +49 -0
  109. package/src/values.config.b.js +49 -0
  110. package/src/values.config.c.js +49 -0
  111. package/src/values.config.d.js +49 -0
  112. package/src/values.config.js +49 -0
  113. package/src/webapi/featureDefinitionActions.js +0 -0
  114. package/src/webapi/featuresActions.js +90 -0
  115. package/src/webapi/helper.js +4 -0
  116. package/src/webapi/index.js +12 -0
  117. package/src/webapi/listingActions.js +176 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Form Actions for Feature Builder
3
+ * Manages form state including title, icon, fields, layout, and submission
4
+ * Handles CRUD operations for form fields and layout configuration
5
+ * Coordinates with external menu updates and feature definition actions
6
+ *
7
+ * @namespace formActions
8
+ */
9
+
10
+ import { featureDefinitionActions } from "../webapi/featuresActions";
11
+ import { values } from "../values.config";
12
+ import {
13
+ updateFeatureBuilderString,
14
+ updateFeatureBuilderIcon,
15
+ } from "./featureBuilderStringsActions";
16
+ import { toTitleCase } from "../utils/textUtils";
17
+
18
+ /**
19
+ * @typedef {Object} FieldValues
20
+ * @property {string} [label] - Field label text
21
+ * @property {string} [placeholder] - Placeholder text for input
22
+ * @property {boolean} [isRequired] - Whether field is required
23
+ * @property {string} [helpText] - Help text for field guidance
24
+ * @property {boolean} [allowCaption] - Whether field allows captions
25
+ * @property {boolean} [useAsSummary] - Whether field is used as summary
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} FormField
30
+ * @property {string} id - Unique field identifier
31
+ * @property {string} type - Field type (text, title, description, image, file, cta, feature-image)
32
+ * @property {boolean} isMandatory - Whether field is mandatory system field
33
+ * @property {FieldValues} values - Field configuration values
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} LayoutConfig
38
+ * @property {string} type - Layout type (round, square, etc.)
39
+ * @property {string} [gridIcon] - Background image for grid layout
40
+ */
41
+
42
+ export const actionsTypes = {
43
+ SET_INITIAL_VALUES: "SET_INITIAL_VALUES",
44
+ SET_TITLE: "SET_TITLE",
45
+ SET_ICON: "SET_ICON",
46
+ SET_DISPLAY_NAME: "SET_DISPLAY_NAME",
47
+ ADD_FIELD: "ADD_FIELD",
48
+ DELETE_FIELD: "DELETE_FIELD",
49
+ UPDATE_FIELD: "UPDATE_FIELD",
50
+ SET_LAYOUT_GRID_ICON: "SET_LAYOUT_GRID_ICON",
51
+ SET_LAYOUT_TYPE: "SET_LAYOUT_TYPE",
52
+ SUBMIT_FORM_REQUEST: "SUBMIT_FORM_REQUEST",
53
+ SUBMIT_FORM_SUCCESS: "SUBMIT_FORM_SUCCESS",
54
+ SUBMIT_FORM_FAILURE: "SUBMIT_FORM_FAILURE",
55
+ CLEAR_FORM_SUBMISSION_STATE: "CLEAR_FORM_SUBMISSION_STATE",
56
+ SET_SUMMARY_FIELD: "SET_SUMMARY_FIELD",
57
+ };
58
+
59
+ /**
60
+ * Action creator to set initial form values
61
+ * Initializes form state with existing feature definition data
62
+ *
63
+ * @param {Object} initialValues - Initial form values object
64
+ * @param {string} initialValues.title - Feature title
65
+ * @param {string} initialValues.icon - Feature icon
66
+ * @param {string} initialValues.displayName - Feature display name
67
+ * @param {LayoutConfig} initialValues.layout - Layout configuration
68
+ * @param {FormField[]} initialValues.fields - Form fields array
69
+ * @returns {Object} Redux action object with type and payload
70
+ *
71
+ * @example
72
+ * dispatch(setInitialValues({
73
+ * title: 'Contact Form',
74
+ * icon: 'envelope',
75
+ * fields: []
76
+ * }));
77
+ */
78
+ export const setInitialValues = (initialValues) => {
79
+ return { type: actionsTypes.SET_INITIAL_VALUES, payload: initialValues };
80
+ };
81
+
82
+ export const setTitle = (title) => {
83
+ return { type: actionsTypes.SET_TITLE, payload: title };
84
+ };
85
+
86
+ /**
87
+ * Action creator to set form display name and update menu
88
+ * Updates both form state and external menu string
89
+ * Title cases the display name before updating the strings store
90
+ *
91
+ * @param {string} displayName - New display name for form
92
+ * @returns {Function} Thunk function for Redux
93
+ *
94
+ * @example
95
+ * dispatch(setDisplayName('Contact Information'));
96
+ */
97
+ export const setDisplayName = (displayName) => {
98
+ return (dispatch) => {
99
+ // Update menu string when display name changes (will be title cased in updateFeatureBuilderString)
100
+ dispatch(updateFeatureBuilderString(displayName));
101
+
102
+ // Dispatch the actual action
103
+ dispatch({
104
+ type: actionsTypes.SET_DISPLAY_NAME,
105
+ payload: displayName,
106
+ });
107
+ };
108
+ };
109
+
110
+ /**
111
+ * Action creator to set form icon and update menu
112
+ * Updates both form state and external menu icon
113
+ *
114
+ * @param {string} icon - Icon identifier or FontAwesome icon class
115
+ * @returns {Function} Thunk function for Redux
116
+ *
117
+ * @example
118
+ * dispatch(setIcon('fa-user'));
119
+ */
120
+ export const setIcon = (icon) => {
121
+ return (dispatch) => {
122
+ // Update menu icon when icon changes
123
+ dispatch(updateFeatureBuilderIcon(icon));
124
+
125
+ // Dispatch the actual action
126
+ dispatch({
127
+ type: actionsTypes.SET_ICON,
128
+ payload: icon,
129
+ });
130
+ };
131
+ };
132
+
133
+ /**
134
+ * Action creator to add a new field to the form
135
+ * Generates unique ID and supports custom field types
136
+ *
137
+ * @param {string} [fieldType="text"] - Type of field to add (text, title, description, image, gallery, file, cta, feature-image)
138
+ * @returns {Object} Redux action object with field ID and type
139
+ *
140
+ * @example
141
+ * dispatch(addField('text'));
142
+ * dispatch(addField('image'));
143
+ */
144
+ export const addField = (fieldType = "text") => {
145
+ // Generate a unique ID for the new field
146
+ const fieldId = `custom-field-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
147
+ return {
148
+ type: actionsTypes.ADD_FIELD,
149
+ payload: { id: fieldId, type: fieldType },
150
+ };
151
+ };
152
+
153
+ export const deleteField = (id) => {
154
+ return { type: actionsTypes.DELETE_FIELD, payload: id };
155
+ };
156
+
157
+ export const updateFieldById = (id, updatedField) => {
158
+ return { type: actionsTypes.UPDATE_FIELD, payload: { id, updatedField } };
159
+ };
160
+
161
+ /**
162
+ * Action creator to set a description field as the summary field
163
+ * Ensures only one description field can be marked as summary by
164
+ * automatically unsetting all other description fields when a new one is selected
165
+ *
166
+ * @param {string} fieldId - ID of the description field to set as summary
167
+ * @returns {Object} Redux action object for summary field selection
168
+ *
169
+ * @example
170
+ * dispatch(setSummaryField('field-description-123'));
171
+ */
172
+ export const setSummaryField = (fieldId) => {
173
+ return { type: actionsTypes.SET_SUMMARY_FIELD, payload: fieldId };
174
+ };
175
+
176
+ export const setGridLayoutIcon = (imageUrl) => {
177
+ return { type: actionsTypes.SET_LAYOUT_GRID_ICON, payload: imageUrl };
178
+ };
179
+
180
+ export const setLayoutType = (layoutType) => {
181
+ return { type: actionsTypes.SET_LAYOUT_TYPE, payload: layoutType };
182
+ };
183
+
184
+ const submitFormRequest = () => {
185
+ return { type: actionsTypes.SUBMIT_FORM_REQUEST };
186
+ };
187
+
188
+ const submitFormSuccess = () => {
189
+ return { type: actionsTypes.SUBMIT_FORM_SUCCESS };
190
+ };
191
+
192
+ const submitFormFailure = (error) => {
193
+ return { type: actionsTypes.SUBMIT_FORM_FAILURE, payload: error };
194
+ };
195
+
196
+ export const clearFormSubmissionState = () => {
197
+ return { type: actionsTypes.CLEAR_FORM_SUBMISSION_STATE };
198
+ };
199
+
200
+ /**
201
+ * Submits the complete feature form to the server
202
+ * Handles form validation, API submission, and error handling
203
+ *
204
+ * @returns {Function} Async thunk function for Redux
205
+ * @throws {Error} When form validation fails or API submission encounters error
206
+ */
207
+ export function submitForm() {
208
+ return async (dispatch, getState) => {
209
+ const state = getState()[values.reducerKey];
210
+ const form = state && state.form;
211
+ if (!form) {
212
+ dispatch(
213
+ submitFormFailure(
214
+ new Error(
215
+ "Form data is missing. Please refresh the page and try again.",
216
+ ),
217
+ ),
218
+ );
219
+ return;
220
+ }
221
+
222
+ dispatch(submitFormRequest());
223
+ try {
224
+ // Get site from auth store
225
+ const site = getState().auth.site;
226
+ if (!site) {
227
+ throw new Error(
228
+ "Authentication error: Site context not found. Please refresh and login again.",
229
+ );
230
+ }
231
+
232
+ // Use mode from fetch instead of checking definition ID
233
+ const definitionState = state && state.definition;
234
+ const mode = definitionState && definitionState.mode; // Use stored mode from fetch
235
+ const definitionId = definitionState && definitionState.id;
236
+
237
+ if (mode === "edit") {
238
+ // Always update when in edit mode
239
+ const updatedDefinition = {
240
+ id: definitionId,
241
+ site: site, // Include site from auth store
242
+ featureDefinition: {
243
+ // Wrap in expected structure for backend
244
+ title: form.title,
245
+ icon: form.icon,
246
+ displayName: form.displayName,
247
+ layout: form.layout,
248
+ fields: form.fields,
249
+ },
250
+ };
251
+ await featureDefinitionActions.edit(updatedDefinition, site);
252
+ } else {
253
+ // Always create when in create mode (or mode is undefined/null)
254
+ if (!values.featureId || !site) {
255
+ throw new Error(
256
+ "Authentication error: Missing required context (featureId or site).",
257
+ );
258
+ }
259
+ await featureDefinitionActions.create(
260
+ values.featureId,
261
+ site, // Use actual site from auth store
262
+ {
263
+ title: form.title,
264
+ icon: form.icon,
265
+ displayName: form.displayName,
266
+ layout: form.layout,
267
+ fields: form.fields,
268
+ },
269
+ );
270
+ }
271
+
272
+ dispatch(submitFormSuccess());
273
+ } catch (err) {
274
+ // Handle different types of errors
275
+ let errorToDisplay = err;
276
+
277
+ if (err.response) {
278
+ // API error (400, 401, 404, 500, etc.)
279
+ const { status, data } = err.response;
280
+ if (status === 400 && data && data.error) {
281
+ errorToDisplay = new Error(`Validation error: ${data.error}`);
282
+ } else if (status === 401) {
283
+ errorToDisplay = new Error(
284
+ "You are not authorized to perform this action",
285
+ );
286
+ } else if (status === 404) {
287
+ errorToDisplay = new Error("Feature definition not found");
288
+ } else if (status >= 500) {
289
+ errorToDisplay = new Error("Server error. Please try again later.");
290
+ } else {
291
+ errorToDisplay = new Error(
292
+ (data && data.error) || `Request failed with status ${status}`,
293
+ );
294
+ }
295
+ } else if (err.request) {
296
+ // Network error (no response received)
297
+ errorToDisplay = new Error(
298
+ "Network error. Please check your connection and try again.",
299
+ );
300
+ } else if (err.message) {
301
+ // Other JavaScript errors
302
+ errorToDisplay = err;
303
+ } else {
304
+ // Unknown error
305
+ errorToDisplay = new Error("An unexpected error occurred while saving");
306
+ }
307
+
308
+ dispatch(submitFormFailure(errorToDisplay));
309
+ }
310
+ };
311
+ }
@@ -0,0 +1,12 @@
1
+ import { PlussCore } from "../feature.config";
2
+
3
+ const { Actions } = PlussCore;
4
+ export const setAuth = Actions.setAuth;
5
+ export const usersLoaded = Actions.usersLoaded;
6
+ export const addRecentlyCreated = Actions.addRecentlyCreated;
7
+ export const setNavData = Actions.setNavData;
8
+
9
+ // Feature Builder specific actions
10
+ export * from "./featureDefinitionsIndex";
11
+ export * from "./listingActions";
12
+ export * from "./formActions";
@@ -0,0 +1,350 @@
1
+ import { arrayMove } from "@dnd-kit/sortable";
2
+ import { listingActions as webApiActions } from "../webapi/listingActions";
3
+
4
+ // Action Types
5
+ export const FETCH_LISTING_REQUEST = "FETCH_LISTING_REQUEST";
6
+ export const FETCH_LISTING_SUCCESS = "FETCH_LISTING_SUCCESS";
7
+ export const FETCH_LISTING_FAILURE = "FETCH_LISTING_FAILURE";
8
+ export const FETCH_LISTING_SILENT_SUCCESS = "FETCH_LISTING_SILENT_SUCCESS";
9
+ export const FETCH_LISTING_SILENT_FAILURE = "FETCH_LISTING_SILENT_FAILURE";
10
+
11
+ export const REORDER_LISTING_SUCCESS = "REORDER_LISTING_SUCCESS";
12
+ export const REORDER_LISTING_FAILURE = "REORDER_LISTING_FAILURE";
13
+
14
+ export const DELETE_LISTING_REQUEST = "DELETE_LISTING_REQUEST";
15
+ export const DELETE_LISTING_SUCCESS = "DELETE_LISTING_SUCCESS";
16
+ export const DELETE_LISTING_FAILURE = "DELETE_LISTING_FAILURE";
17
+
18
+ export const UNDELETE_LISTING_REQUEST = "UNDELETE_LISTING_REQUEST";
19
+ export const UNDELETE_LISTING_SUCCESS = "UNDELETE_LISTING_SUCCESS";
20
+ export const UNDELETE_LISTING_FAILURE = "UNDELETE_LISTING_FAILURE";
21
+
22
+ export const CREATE_LISTING_REQUEST = "CREATE_LISTING_REQUEST";
23
+ export const CREATE_LISTING_SUCCESS = "CREATE_LISTING_SUCCESS";
24
+ export const CREATE_LISTING_FAILURE = "CREATE_LISTING_FAILURE";
25
+
26
+ export const EDIT_LISTING_REQUEST = "EDIT_LISTING_REQUEST";
27
+ export const EDIT_LISTING_SUCCESS = "EDIT_LISTING_SUCCESS";
28
+ export const EDIT_LISTING_FAILURE = "EDIT_LISTING_FAILURE";
29
+
30
+ export const TOGGLE_LISTING_REQUEST = "TOGGLE_LISTING_REQUEST";
31
+ export const TOGGLE_LISTING_SUCCESS = "TOGGLE_LISTING_SUCCESS";
32
+ export const TOGGLE_LISTING_FAILURE = "TOGGLE_LISTING_FAILURE";
33
+
34
+ export const SET_SORT_BY = "SET_SORT_BY";
35
+ export const SET_SHOW_DELETED = "SET_SHOW_DELETED";
36
+
37
+ export const fetchListingRequest = () => ({ type: FETCH_LISTING_REQUEST });
38
+ export const fetchListingSuccess = (listings) => ({
39
+ type: FETCH_LISTING_SUCCESS,
40
+ payload: listings,
41
+ });
42
+ export const fetchListingFailure = (error) => ({
43
+ type: FETCH_LISTING_FAILURE,
44
+ payload: error,
45
+ });
46
+
47
+ export const fetchListingSilentSuccess = (listings) => ({
48
+ type: FETCH_LISTING_SILENT_SUCCESS,
49
+ payload: listings,
50
+ });
51
+
52
+ export const fetchListingSilentFailure = (error) => ({
53
+ type: FETCH_LISTING_SILENT_FAILURE,
54
+ payload: error,
55
+ });
56
+
57
+ export const fetchListing = () => {
58
+ return async (dispatch, getState) => {
59
+ dispatch(fetchListingRequest());
60
+ try {
61
+ // Get site from auth store
62
+ const site = getState().auth.site;
63
+ if (!site) {
64
+ throw new Error(
65
+ "Authentication error: Site context not found. Please refresh and login again.",
66
+ );
67
+ }
68
+ const response = await webApiActions.getAll(site);
69
+ // Assuming your API returns the listings directly or in a `data` property
70
+ const listings = response.data.listings;
71
+
72
+ dispatch(fetchListingSuccess(listings));
73
+ } catch (error) {
74
+ dispatch(
75
+ fetchListingFailure(error.message || "Failed to fetch listings"),
76
+ );
77
+ }
78
+ };
79
+ };
80
+
81
+ export const fetchListingSilent = () => {
82
+ return async (dispatch, getState) => {
83
+ try {
84
+ // Get site from auth store
85
+ const site = getState().auth.site;
86
+ if (!site) {
87
+ throw new Error(
88
+ "Authentication error: Site context not found. Please refresh and login again.",
89
+ );
90
+ }
91
+ const response = await webApiActions.getAll(site);
92
+ // Assuming your API returns the listings directly or in a `data` property
93
+ const listings = response.data.listings;
94
+
95
+ dispatch(fetchListingSilentSuccess(listings));
96
+ } catch (error) {
97
+ dispatch(
98
+ fetchListingSilentFailure(error.message || "Failed to fetch listings"),
99
+ );
100
+ }
101
+ };
102
+ };
103
+
104
+ export const fetchSingleListing = (id) => {
105
+ return async (dispatch, getState) => {
106
+ dispatch(fetchListingRequest());
107
+ try {
108
+ // Get site from auth store
109
+ const site = getState().auth.site;
110
+ if (!site) {
111
+ throw new Error(
112
+ "Authentication error: Site context not found. Please refresh and login again.",
113
+ );
114
+ }
115
+ const response = await webApiActions.getSingle(id, site);
116
+ dispatch(fetchListingSuccess([response.data])); // Return as array for consistency
117
+ } catch (error) {
118
+ dispatch(fetchListingFailure(error.message || "Failed to fetch listing"));
119
+ }
120
+ };
121
+ };
122
+
123
+ export const reorderListingSuccess = (listings) => ({
124
+ type: REORDER_LISTING_SUCCESS,
125
+ payload: listings,
126
+ });
127
+ export const reorderListingFailure = (error) => ({
128
+ type: REORDER_LISTING_FAILURE,
129
+ payload: error,
130
+ });
131
+
132
+ export const reorderListing = (listings, oldIndex, newIndex) => {
133
+ return async (dispatch) => {
134
+ // Optimistic UI update: update the state immediately
135
+ const reorderedListings = arrayMove(listings, oldIndex, newIndex);
136
+ dispatch(reorderListingSuccess(reorderedListings));
137
+
138
+ try {
139
+ // Note: Reorder functionality not available in current API spec
140
+ // This would need to be implemented if needed
141
+ // await listingActions.updateOrder(reorderedListings);
142
+ } catch (error) {
143
+ // On failure, you might want to revert the state.
144
+ dispatch(
145
+ reorderListingFailure(error.message || "Failed to save new order"),
146
+ );
147
+ }
148
+ };
149
+ };
150
+
151
+ export const deleteListingRequest = (id) => ({
152
+ type: DELETE_LISTING_REQUEST,
153
+ payload: id,
154
+ });
155
+ export const deleteListingSuccess = (id) => ({
156
+ type: DELETE_LISTING_SUCCESS,
157
+ payload: id,
158
+ });
159
+ export const deleteListingFailure = (error) => ({
160
+ type: DELETE_LISTING_FAILURE,
161
+ payload: error,
162
+ });
163
+
164
+ export const deleteListing = (id) => {
165
+ return async (dispatch, getState) => {
166
+ dispatch(deleteListingRequest(id));
167
+ try {
168
+ // Get site from auth store
169
+ const site = getState().auth.site;
170
+ if (!site) {
171
+ throw new Error(
172
+ "Authentication error: Site context not found. Please refresh and login again.",
173
+ );
174
+ }
175
+ await webApiActions.delete(id, site);
176
+ dispatch(deleteListingSuccess(id));
177
+ return { success: true };
178
+ } catch (error) {
179
+ const errorMessage = error.message || "Failed to delete listing";
180
+ dispatch(deleteListingFailure(errorMessage));
181
+ throw new Error(errorMessage);
182
+ }
183
+ };
184
+ };
185
+
186
+ export const undeleteListingRequest = (id) => ({
187
+ type: UNDELETE_LISTING_REQUEST,
188
+ payload: id,
189
+ });
190
+ export const undeleteListingSuccess = (listing) => ({
191
+ type: UNDELETE_LISTING_SUCCESS,
192
+ payload: listing,
193
+ });
194
+ export const undeleteListingFailure = (error) => ({
195
+ type: UNDELETE_LISTING_FAILURE,
196
+ payload: error,
197
+ });
198
+
199
+ export const undeleteListing = (id) => {
200
+ return async (dispatch, getState) => {
201
+ dispatch(undeleteListingRequest(id));
202
+ try {
203
+ // Get site from auth store
204
+ const site = getState().auth.site;
205
+ if (!site) {
206
+ throw new Error(
207
+ "Authentication error: Site context not found. Please refresh and login again.",
208
+ );
209
+ }
210
+ const response = await webApiActions.undelete(id, site);
211
+ // If API returns the restored listing, use it; otherwise we'll need to refetch
212
+ const restoredListing = response.data;
213
+ if (restoredListing && restoredListing.id) {
214
+ dispatch(undeleteListingSuccess(restoredListing));
215
+ } else {
216
+ // Trigger a refetch by dispatching the success with just ID, then fetch updated listings
217
+ dispatch(undeleteListingSuccess(id));
218
+ // Fetch updated listings to get the restored one
219
+ dispatch(fetchListingSilent());
220
+ }
221
+ return { success: true };
222
+ } catch (error) {
223
+ const errorMessage = error.message || "Failed to restore listing";
224
+ dispatch(undeleteListingFailure(errorMessage));
225
+ throw new Error(errorMessage);
226
+ }
227
+ };
228
+ };
229
+
230
+ export const createListingRequest = () => ({ type: CREATE_LISTING_REQUEST });
231
+ export const createListingSuccess = (listing) => ({
232
+ type: CREATE_LISTING_SUCCESS,
233
+ payload: listing,
234
+ });
235
+ export const createListingFailure = (error) => ({
236
+ type: CREATE_LISTING_FAILURE,
237
+ payload: error,
238
+ });
239
+
240
+ export const createListing = (listing) => {
241
+ return async (dispatch, getState) => {
242
+ dispatch(createListingRequest());
243
+ try {
244
+ // Get site from auth store
245
+ const site = getState().auth.site;
246
+ if (!site) {
247
+ throw new Error(
248
+ "Authentication error: Site context not found. Please refresh and login again.",
249
+ );
250
+ }
251
+ const response = await webApiActions.create(listing, site);
252
+
253
+ // Assuming the API returns the newly created listing
254
+ const newListing = response.data || response;
255
+ dispatch(createListingSuccess(newListing));
256
+ return newListing;
257
+ } catch (error) {
258
+ const errorMessage =
259
+ error.response?.data?.message ||
260
+ error.message ||
261
+ "Failed to create listing";
262
+ dispatch(createListingFailure(errorMessage));
263
+ throw error; // Re-throw to allow component to handle
264
+ }
265
+ };
266
+ };
267
+
268
+ export const editListingRequest = () => ({ type: EDIT_LISTING_REQUEST });
269
+ export const editListingSuccess = (listing) => ({
270
+ type: EDIT_LISTING_SUCCESS,
271
+ payload: listing,
272
+ });
273
+ export const editListingFailure = (error) => ({
274
+ type: EDIT_LISTING_FAILURE,
275
+ payload: error,
276
+ });
277
+
278
+ export const editListing = (listing) => {
279
+ return async (dispatch, getState) => {
280
+ dispatch(editListingRequest());
281
+ try {
282
+ // Get site from auth store
283
+ const site = getState().auth.site;
284
+ if (!site) {
285
+ throw new Error(
286
+ "Authentication error: Site context not found. Please refresh and login again.",
287
+ );
288
+ }
289
+ const response = await webApiActions.edit(listing, site);
290
+ const updatedListing = response.data || response;
291
+ dispatch(editListingSuccess(updatedListing));
292
+ return updatedListing;
293
+ } catch (error) {
294
+ const errorMessage =
295
+ error.response?.data?.message ||
296
+ error.message ||
297
+ "Failed to update listing";
298
+ dispatch(editListingFailure(errorMessage));
299
+ throw error; // Re-throw to allow component to handle
300
+ }
301
+ };
302
+ };
303
+
304
+ export const toggleListingRequest = (id) => ({
305
+ type: TOGGLE_LISTING_REQUEST,
306
+ payload: id,
307
+ });
308
+
309
+ export const toggleListingSuccess = (id, isActive) => ({
310
+ type: TOGGLE_LISTING_SUCCESS,
311
+ payload: { id, isActive },
312
+ });
313
+
314
+ export const toggleListingFailure = (error) => ({
315
+ type: TOGGLE_LISTING_FAILURE,
316
+ payload: error,
317
+ });
318
+
319
+ export const toggleListing = (id, isActive) => {
320
+ return async (dispatch, getState) => {
321
+ dispatch(toggleListingRequest(id));
322
+ try {
323
+ // Get site from auth store
324
+ const site = getState().auth.site;
325
+ if (!site) {
326
+ throw new Error(
327
+ "Authentication error: Site context not found. Please refresh and login again.",
328
+ );
329
+ }
330
+ // Update the listing with the new active state
331
+ await webApiActions.edit({ id, isActive }, site);
332
+ dispatch(toggleListingSuccess(id, isActive));
333
+ return { success: true };
334
+ } catch (error) {
335
+ const errorMessage = error.message || "Failed to toggle listing";
336
+ dispatch(toggleListingFailure(errorMessage));
337
+ throw new Error(errorMessage);
338
+ }
339
+ };
340
+ };
341
+
342
+ export const setSortBy = (sortBy) => ({
343
+ type: SET_SORT_BY,
344
+ payload: sortBy,
345
+ });
346
+
347
+ export const setShowDeleted = (showDeleted) => ({
348
+ type: SET_SHOW_DELETED,
349
+ payload: showDeleted,
350
+ });