@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,406 @@
1
+ import React, { useState, Fragment } from "react";
2
+ import { useSelector, useDispatch } from "react-redux";
3
+ import { PlussCore } from "../feature.config";
4
+ import { capitalizeTextWithFallback } from "../utils/textUtils";
5
+ import ListingForm from "../components/listing/ListingForm.jsx";
6
+ import ToastContainer from "./ToastContainer.jsx";
7
+ import styles from "./ListingEditor.module.css";
8
+ import {
9
+ OverlayPage,
10
+ OverlayPageContents,
11
+ OverlayPageBottomButtons,
12
+ OverlayPageSection,
13
+ Text,
14
+ Button,
15
+ SkeletonLoader,
16
+ LoadingState,
17
+ } from "../components";
18
+ import {
19
+ selectDefinition,
20
+ selectDefinitionIsLoading,
21
+ selectListingsIsLoading,
22
+ selectDefinitionError,
23
+ selectHasDefinition,
24
+ selectFormDisplayName,
25
+ selectListingById,
26
+ } from "../selectors/featureBuilderSelectors";
27
+ import {
28
+ createListing,
29
+ editListing,
30
+ fetchSingleListing,
31
+ } from "../actions/listingActions";
32
+ import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex.js";
33
+ import { values } from "../values.config.js";
34
+
35
+
36
+ const ListingEditor = ({ mode = "create", listingId, onSuccess, onCancel }) => {
37
+ const isEditMode = mode === "edit";
38
+ const dispatch = useDispatch();
39
+ const auth = useSelector((state) => state.auth);
40
+ const featureDefinition = useSelector(selectDefinition);
41
+ const displayName =
42
+ useSelector(selectFormDisplayName) || featureDefinition?.displayName;
43
+ const isLoading = useSelector(selectDefinitionIsLoading);
44
+ const listingsIsLoading = useSelector(selectListingsIsLoading);
45
+ const error = useSelector(selectDefinitionError);
46
+ const hasDefinition = useSelector(selectHasDefinition);
47
+ const listing = useSelector(selectListingById(listingId));
48
+
49
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
50
+ const [formData, setFormData] = React.useState({ fields: {} });
51
+ const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
52
+ const [formErrors, setFormErrors] = React.useState({});
53
+ const [showErrors, setShowErrors] = React.useState(false);
54
+ const [formErrorMessage, setFormErrorMessage] = React.useState(null);
55
+ const [toasts, setToasts] = React.useState([]);
56
+ const formRef = React.useRef(null);
57
+
58
+ // Toast management functions
59
+ const addToast = (type, message) => {
60
+ const id = Date.now();
61
+ setToasts((prev) => [...prev, { id, type, message, isVisible: true }]);
62
+ };
63
+
64
+ const removeToast = (id) => {
65
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
66
+ };
67
+
68
+ // Fetch listing data in edit mode
69
+ React.useEffect(() => {
70
+ if (isEditMode && listingId) {
71
+ // Check if listing data already exists in Redux store
72
+ if (!listing) {
73
+ dispatch(fetchSingleListing(listingId));
74
+ }
75
+ }
76
+ }, [dispatch, isEditMode, listingId, listing]);
77
+
78
+ // Pre-fill form data in edit mode
79
+ React.useEffect(() => {
80
+ if (isEditMode && listing) {
81
+ // Always use listing.fields for consistency - never use the entire listing object
82
+ const fields = listing.fields || {};
83
+ setFormData({ fields });
84
+ }
85
+ }, [isEditMode, listing]);
86
+
87
+ const handleSubmit = (listingData) => {
88
+ // listingData is expected to have the structure { fields: { fieldId: value, ... } }
89
+ // After our fixes, form should always send { fields: {...} } structure
90
+ if (!listingData || !listingData.fields) {
91
+ return;
92
+ }
93
+
94
+ const newFormData = { fields: listingData.fields };
95
+
96
+ setFormData(newFormData);
97
+ setHasUnsavedChanges(true);
98
+ // Clear errors when form data is updated
99
+ setFormErrors({});
100
+ setShowErrors(false);
101
+ setFormErrorMessage(null);
102
+ };
103
+
104
+ const handleSave = () => {
105
+ // Validate form - formData should always have structure { fields: {...} }
106
+ if (!formData || !formData.fields) {
107
+ setFormErrorMessage("Form data is invalid. Please try again.");
108
+ addToast("error", "Form data is invalid. Please try again.");
109
+ return;
110
+ }
111
+ setFormErrorMessage(null); // Clear previous error when validation passes
112
+
113
+ const formFields = formData.fields;
114
+ const errors = {};
115
+
116
+ // Check all fields to collect all missing required fields
117
+ featureDefinition.fields.forEach((field) => {
118
+ // Use field.values.isRequired consistently (not field.isMandatory)
119
+ if (field.values?.isRequired) {
120
+ const fieldValue = formFields[field.id];
121
+ let isEmpty = false;
122
+
123
+ // Handle different field types based on their value structure
124
+ if (field.type === "cta") {
125
+ // CTA fields have object structure { label: "...", url: "..." }
126
+ if (!fieldValue || typeof fieldValue !== "object") {
127
+ isEmpty = true;
128
+ } else {
129
+ // Check if both label and url are provided and non-empty
130
+ const hasLabel =
131
+ fieldValue.label && fieldValue.label.trim().length > 0;
132
+ const hasUrl = fieldValue.url && fieldValue.url.trim().length > 0;
133
+ isEmpty = !hasLabel || !hasUrl;
134
+ }
135
+ } else if (field.type === "file") {
136
+ // File fields can be arrays (multiple files) or objects (single file)
137
+ if (!fieldValue) {
138
+ isEmpty = true;
139
+ } else if (Array.isArray(fieldValue)) {
140
+ isEmpty = fieldValue.length === 0;
141
+ } else if (typeof fieldValue === "object") {
142
+ isEmpty = Object.keys(fieldValue).length === 0;
143
+ } else {
144
+ isEmpty =
145
+ typeof fieldValue === "string" && fieldValue.trim() === "";
146
+ }
147
+ } else {
148
+ // Handle string values for other field types
149
+ isEmpty =
150
+ !fieldValue ||
151
+ (typeof fieldValue === "string" && fieldValue.trim() === "");
152
+ }
153
+
154
+ if (isEmpty) {
155
+ errors[field.id] = `${field.values?.label || field.id} is required`;
156
+ }
157
+ }
158
+ });
159
+
160
+ const isValid = Object.keys(errors).length === 0;
161
+
162
+ if (!isValid) {
163
+ setFormErrors(errors);
164
+ setShowErrors(true);
165
+ return;
166
+ }
167
+
168
+ setIsSubmitting(true);
169
+
170
+ // formData always has structure { fields: {...} } after our fixes
171
+ const submissionData = isEditMode
172
+ ? { id: listingId, fields: formData.fields }
173
+ : { fields: formData.fields };
174
+
175
+ const actionPromise = dispatch(
176
+ isEditMode ? editListing(submissionData) : createListing(submissionData),
177
+ );
178
+
179
+ actionPromise
180
+ .then((result) => {
181
+ setIsSubmitting(false);
182
+ setHasUnsavedChanges(false);
183
+
184
+ // Call onSuccess callback with listing ID if available
185
+ if (onSuccess) {
186
+ const createdId = result?.data?.id || listingId;
187
+ onSuccess(createdId);
188
+ }
189
+ })
190
+ .catch((error) => {
191
+ const errorMessage = `Failed to ${
192
+ isEditMode ? "update" : "create"
193
+ } listing. Please try again.`;
194
+ addToast("error", errorMessage);
195
+ setIsSubmitting(false);
196
+ });
197
+ };
198
+
199
+ const handleClose = () => {
200
+ if (hasUnsavedChanges && onCancel) {
201
+ if (
202
+ window.confirm(
203
+ "You have unsaved changes. Are you sure you want to leave?",
204
+ )
205
+ ) {
206
+ onCancel();
207
+ }
208
+ } else if (onCancel) {
209
+ onCancel();
210
+ }
211
+ };
212
+
213
+ // Check for content management permission
214
+ if (
215
+ !PlussCore.Session.validateAccess(
216
+ auth.site,
217
+ values.permissionFeatureBuilderContent,
218
+ auth,
219
+ )
220
+ ) {
221
+ return (
222
+ <OverlayPage onClose={handleClose}>
223
+ <OverlayPageContents>
224
+ <OverlayPageSection className="pageSectionWrapper--newPopup1000">
225
+ <div className={`padding-20 ${styles.centered}`}>
226
+ <Text type="h2">Access Restricted</Text>
227
+ <Text type="body">
228
+ You don't have permission to {isEditMode ? "edit" : "create"}{" "}
229
+ feature content.
230
+ </Text>
231
+ <Text type="body">
232
+ Please contact your administrator if you need access.
233
+ </Text>
234
+ </div>
235
+ </OverlayPageSection>
236
+ </OverlayPageContents>
237
+ <OverlayPageBottomButtons>
238
+ <Button buttonType="secondary" onClick={handleClose}>
239
+ Close
240
+ </Button>
241
+ </OverlayPageBottomButtons>
242
+ </OverlayPage>
243
+ );
244
+ }
245
+
246
+ // Show loading state only when fetching initial feature definition
247
+ // Don't show skeleton when creating new listings since we have the definition in the store
248
+ if (!hasDefinition && isLoading) {
249
+ return (
250
+ <OverlayPage onClose={handleClose}>
251
+ <OverlayPageContents>
252
+ <OverlayPageSection className="pageSectionWrapper--newPopup1000">
253
+ <div className="padding-20">
254
+ <SkeletonLoader type="form-input" count={4} />
255
+ </div>
256
+ </OverlayPageSection>
257
+ </OverlayPageContents>
258
+ <OverlayPageBottomButtons>
259
+ <SkeletonLoader type="button" count={2} />
260
+ </OverlayPageBottomButtons>
261
+ </OverlayPage>
262
+ );
263
+ }
264
+
265
+ if (error) {
266
+ return (
267
+ <OverlayPage onClose={handleClose}>
268
+ <OverlayPageContents>
269
+ <OverlayPageSection className="pageSectionWrapper--newPopup1000">
270
+ <div className={`padding-20 ${styles.centered}`}>
271
+ <Text type="body">Error loading data: {error}</Text>
272
+ </div>
273
+ </OverlayPageSection>
274
+ </OverlayPageContents>
275
+ <OverlayPageBottomButtons>
276
+ <Button
277
+ buttonType="primary"
278
+ onClick={() => {
279
+ dispatch(fetchFeatureDefinitions());
280
+ if (isEditMode && listingId) {
281
+ dispatch(fetchSingleListing(listingId));
282
+ }
283
+ }}
284
+ >
285
+ Retry
286
+ </Button>
287
+ <Button buttonType="secondary" onClick={handleClose}>
288
+ Close
289
+ </Button>
290
+ </OverlayPageBottomButtons>
291
+ </OverlayPage>
292
+ );
293
+ }
294
+
295
+ if (!hasDefinition) {
296
+ return (
297
+ <OverlayPage onClose={handleClose}>
298
+ <OverlayPageContents>
299
+ <OverlayPageSection className="pageSectionWrapper--newPopup1000">
300
+ <div className={`padding-20 ${styles.centered}`}>
301
+ <Text type="h2">No Feature Definition</Text>
302
+ <Text type="body">
303
+ No feature definition exists. Please create a feature definition
304
+ first.
305
+ </Text>
306
+ </div>
307
+ </OverlayPageSection>
308
+ </OverlayPageContents>
309
+ <OverlayPageBottomButtons>
310
+ <Button buttonType="secondary" onClick={handleClose}>
311
+ Close
312
+ </Button>
313
+ </OverlayPageBottomButtons>
314
+ </OverlayPage>
315
+ );
316
+ }
317
+
318
+ return (
319
+ <Fragment>
320
+ <OverlayPage onClose={handleClose}>
321
+ <OverlayPageContents>
322
+ <OverlayPageSection className="pageSectionWrapper--newPopup">
323
+ <Text type="formTitleLarge" className="marginBottom-16">
324
+ {isEditMode
325
+ ? `Edit ${capitalizeTextWithFallback(displayName, values.singularName)} listing`
326
+ : `Create new ${capitalizeTextWithFallback(displayName, "listing")}`}
327
+ </Text>
328
+
329
+ {/* Form Section */}
330
+ <div ref={formRef}>
331
+ {isEditMode && !listing && listingsIsLoading ? (
332
+ <div
333
+ className={`padding-20 ${styles.centered} ${styles.loadingContainer}`}
334
+ >
335
+ <LoadingState message="Loading listing..." />
336
+ </div>
337
+ ) : isEditMode && !listing ? (
338
+ <div
339
+ className={`padding-20 ${styles.centered} ${styles.loadingContainer}`}
340
+ >
341
+ <Text type="body">
342
+ Listing not found or has been deleted.
343
+ </Text>
344
+ <Button
345
+ buttonType="secondary"
346
+ onClick={handleClose}
347
+ className={styles.backButton}
348
+ >
349
+ Go Back
350
+ </Button>
351
+ </div>
352
+ ) : (
353
+ <ListingForm
354
+ featureDefinition={featureDefinition}
355
+ onSubmit={handleSubmit}
356
+ mode={isEditMode ? "edit" : "create"}
357
+ initialData={
358
+ isEditMode && listing
359
+ ? { fields: listing.fields || {} }
360
+ : undefined
361
+ }
362
+ onChange={() => setHasUnsavedChanges(true)}
363
+ disabled={isSubmitting}
364
+ hideSubmitButton={true}
365
+ formData={formData.fields}
366
+ setFormData={(fields) => setFormData({ fields })}
367
+ formErrors={formErrors}
368
+ showErrors={showErrors}
369
+ formErrorMessage={formErrorMessage}
370
+ />
371
+ )}
372
+ </div>
373
+ </OverlayPageSection>
374
+ </OverlayPageContents>
375
+
376
+ <OverlayPageBottomButtons>
377
+ <Button
378
+ buttonType="secondary"
379
+ onClick={handleClose}
380
+ disabled={isSubmitting}
381
+ className="marginRight-16"
382
+ >
383
+ Cancel
384
+ </Button>
385
+ <Button
386
+ buttonType="primary"
387
+ onClick={handleSave}
388
+ disabled={isSubmitting}
389
+ isActive
390
+ >
391
+ {isSubmitting
392
+ ? isEditMode
393
+ ? "Saving..."
394
+ : "Creating..."
395
+ : isEditMode
396
+ ? "Save Changes"
397
+ : "Create Listing"}
398
+ </Button>
399
+ </OverlayPageBottomButtons>
400
+ </OverlayPage>
401
+ <ToastContainer toasts={toasts} onDismiss={removeToast} />
402
+ </Fragment>
403
+ );
404
+ };
405
+
406
+ export default ListingEditor;
@@ -0,0 +1,14 @@
1
+ /* Centered content */
2
+ .centered {
3
+ text-align: center;
4
+ }
5
+
6
+ /* Loading container */
7
+ .loadingContainer {
8
+ padding: 60px 20px;
9
+ }
10
+
11
+ /* Back button margin */
12
+ .backButton {
13
+ margin-top: 16px;
14
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { PlussCore } from "../feature.config";
3
+ const { Components } = PlussCore;
4
+ const { Popup } = Components;
5
+
6
+ const ListingSuccessPopup = ({ isOpen, onClose, mode, listingId }) => {
7
+ if (!isOpen) {
8
+ return null;
9
+ }
10
+
11
+ const isCreateMode = mode === "create";
12
+ const title = isCreateMode ? "Listing Created" : "Listing Updated";
13
+ const subtitle = isCreateMode
14
+ ? "Your listing has been created and is now available."
15
+ : "Your changes have been saved and are now reflected.";
16
+
17
+ const buttons = [
18
+ {
19
+ type: "primary",
20
+ text: "View All Listings",
21
+ onClick: onClose,
22
+ isActive: true,
23
+ },
24
+ ];
25
+
26
+ // Add "Create Another" button for create mode
27
+ if (isCreateMode) {
28
+ buttons.unshift({
29
+ type: "secondary",
30
+ text: "Create Another",
31
+ onClick: () => {
32
+ // Navigate to create new listing
33
+ onClose(); // Close the popup first
34
+ window.location.href = "/feature-builder/listing/create";
35
+ },
36
+ });
37
+ }
38
+
39
+ return (
40
+ <Popup
41
+ title={title}
42
+ subtitle={subtitle}
43
+ onClose={onClose}
44
+ buttons={buttons}
45
+ hasPadding
46
+ minWidth={400}
47
+ maxWidth={500}
48
+ ></Popup>
49
+ );
50
+ };
51
+
52
+ export { ListingSuccessPopup };
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faSpinner } from "@fortawesome/free-solid-svg-icons";
4
+ import styles from "./LoadingScreen.module.css";
5
+
6
+ const LoadingScreen = ({
7
+ message = "Loading...",
8
+ size = "large",
9
+ showBackground = true,
10
+ type = "default", // default, feature-definition, listings, navigation
11
+ }) => {
12
+ // Get contextual message based on type
13
+ const getContextualMessage = () => {
14
+ if (message !== "Loading...") return message;
15
+
16
+ switch (type) {
17
+ case "feature-definition":
18
+ return "Loading feature definition...";
19
+ case "listings":
20
+ return "Loading your listings...";
21
+ case "navigation":
22
+ return "Loading next step...";
23
+ default:
24
+ return "Loading...";
25
+ }
26
+ };
27
+
28
+ const sizeClass =
29
+ size === "small"
30
+ ? styles.small
31
+ : size === "medium"
32
+ ? styles.medium
33
+ : styles.large;
34
+
35
+ return (
36
+ <div
37
+ className={`${styles.loadingScreen} ${showBackground ? styles.withBackground : ""}`}
38
+ >
39
+ <div className={styles.loadingContent}>
40
+ <div className={`${styles.spinnerContainer} ${sizeClass}`}>
41
+ <FontAwesomeIcon icon={faSpinner} spin className={styles.spinner} />
42
+ </div>
43
+ <div className={styles.loadingText}>{getContextualMessage()}</div>
44
+ {type !== "default" && (
45
+ <div className={styles.loadingSubtext}>
46
+ This will only take a moment...
47
+ </div>
48
+ )}
49
+ </div>
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export default LoadingScreen;
@@ -0,0 +1,103 @@
1
+ .loadingScreen {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ z-index: 9999;
11
+ pointer-events: none;
12
+ }
13
+
14
+ .loadingScreen.withBackground {
15
+ background: rgba(255, 255, 255, 0.95);
16
+ backdrop-filter: blur(4px);
17
+ pointer-events: all;
18
+ }
19
+
20
+ .loadingContent {
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ justify-content: center;
25
+ text-align: center;
26
+ padding: 3rem 2rem;
27
+ max-width: 400px;
28
+ }
29
+
30
+ .spinnerContainer {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ margin-bottom: 1.5rem;
35
+ }
36
+
37
+ .spinnerContainer.small {
38
+ width: 32px;
39
+ height: 32px;
40
+ }
41
+
42
+ .spinnerContainer.medium {
43
+ width: 48px;
44
+ height: 48px;
45
+ }
46
+
47
+ .spinnerContainer.large {
48
+ width: 64px;
49
+ height: 64px;
50
+ }
51
+
52
+ .spinner {
53
+ color: var(--colour-branding-main, #4a57b7);
54
+ width: 100%;
55
+ height: 100%;
56
+ font-size: inherit;
57
+ }
58
+
59
+ .loadingText {
60
+ font-size: var(--font-size-lg);
61
+ font-weight: 500;
62
+ color: var(--text-dark, #181c4a);
63
+ margin-bottom: 0.5rem;
64
+ line-height: 1.4;
65
+ }
66
+
67
+ .loadingSubtext {
68
+ font-size: var(--font-size-sm);
69
+ color: var(--text-light, #717b81);
70
+ margin-top: 0.5rem;
71
+ line-height: 1.4;
72
+ }
73
+
74
+ /* Animation for smooth transitions */
75
+ .loadingScreen {
76
+ opacity: 0;
77
+ transition: opacity 0.3s ease-in-out;
78
+ }
79
+
80
+ .loadingScreen.withBackground {
81
+ opacity: 1;
82
+ }
83
+
84
+ /* Responsive adjustments */
85
+ @media (max-width: 768px) {
86
+ .loadingContent {
87
+ padding: 2rem 1rem;
88
+ }
89
+
90
+ .loadingText {
91
+ font-size: var(--font-size-base);
92
+ }
93
+
94
+ .spinnerContainer.large {
95
+ width: 48px;
96
+ height: 48px;
97
+ }
98
+
99
+ .spinnerContainer.medium {
100
+ width: 40px;
101
+ height: 40px;
102
+ }
103
+ }
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import FontAwesome from "react-fontawesome";
3
+ import styles from "./LoadingState.module.css";
4
+
5
+ /**
6
+ * Loading State component for async operations
7
+ * Displays a spinning loader with optional message text
8
+ * Uses FontAwesome spinner with hardcoded pixel sizing to avoid rem scaling issues
9
+ * Consistent styling with info page loader pattern
10
+ *
11
+ * @param {Object} props - Component props
12
+ * @param {string} props.message - Descriptive message to display alongside the spinner
13
+ * @param {number} [props.size=48] - Size of the spinner in pixels
14
+ * @returns {React.ReactElement} Loading state interface with spinner and message
15
+ *
16
+ * @example
17
+ * <LoadingState message="Loading feature data..." />
18
+ *
19
+ * @example
20
+ * <LoadingState
21
+ * message="Processing your request..."
22
+ * size={64}
23
+ * />
24
+ */
25
+ export function LoadingState({ message, size = 48 }) {
26
+ return (
27
+ <div className={styles.loadingState}>
28
+ <FontAwesome
29
+ style={{
30
+ fontSize: size,
31
+ color: "var(--colour-branding-main, #4a57b7)",
32
+ }}
33
+ name="spinner fa-pulse fa-fw"
34
+ />
35
+ <p>{message}</p>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export default LoadingState;
@@ -0,0 +1,18 @@
1
+ /* LoadingState CSS Module - Based on info page loader pattern */
2
+
3
+ .loadingState {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: center;
7
+ justify-content: center;
8
+ gap: 2rem;
9
+ height: 50vh;
10
+ text-align: center;
11
+ }
12
+
13
+ .loadingState p {
14
+ color: var(--text-bluegrey, #6c7a90);
15
+ font-size: 1.6rem;
16
+ font-weight: 300;
17
+ margin-bottom: 0;
18
+ }