@plusscommunities/pluss-feature-builder-web-a 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,192 @@
1
+ /* Enhanced File Input Styles - Based on existing PlussCore FileInput styling */
2
+
3
+ /* Main container for enhanced file input */
4
+ .listingFileInput {
5
+ width: 100%;
6
+ }
7
+
8
+ /* Drop zone area - reusing existing imageInput styling */
9
+ .listingFileInput__dropZone {
10
+ width: 100%;
11
+ margin-bottom: 1rem;
12
+ }
13
+
14
+ /* Drop zone error state */
15
+ .listingFileInput__dropZone--error {
16
+ border: 1px solid var(--colour-error);
17
+ border-radius: var(--border-radius-base);
18
+ padding: 0.25rem;
19
+ background-color: var(--colour-error-light, rgba(220, 53, 69, 0.05));
20
+ }
21
+
22
+ .listingFileInput__dropZone .imageInputOuter-single {
23
+ width: 100%;
24
+ }
25
+
26
+ .listingFileInput__dropZone .imageInput {
27
+ width: 100%;
28
+ margin-right: 0;
29
+ }
30
+
31
+ /* File list container */
32
+ .listingFileInput__fileList {
33
+ margin-top: 1rem;
34
+ }
35
+
36
+ /* Individual file item */
37
+ .fileListItem {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 0.75rem;
41
+ padding: 0.75rem;
42
+ background-color: var(--bg-white);
43
+ margin-bottom: 0.5rem;
44
+ transition: all var(--transition-base) ease-in-out;
45
+ }
46
+
47
+ .fileListItem:hover {
48
+ box-shadow: 0 0 4px var(--colour-branding-action-alpha20);
49
+ }
50
+
51
+ .fileListItem__icon {
52
+ color: var(--text-bluegrey);
53
+ font-size: var(--font-size-base);
54
+ flex-shrink: 0;
55
+ width: 1.5rem;
56
+ text-align: center;
57
+ }
58
+
59
+ .fileListItem__input {
60
+ min-width: 0;
61
+ flex-grow: 1;
62
+ }
63
+
64
+ .fileListItem__nameInput {
65
+ }
66
+
67
+ .fileListItem__originalName {
68
+ flex: 0 1 200px;
69
+ color: var(--text-bluegrey);
70
+ font-size: 16px;
71
+ white-space: nowrap;
72
+ overflow: hidden;
73
+ text-overflow: ellipsis;
74
+ }
75
+
76
+ .fileListItem__actions {
77
+ flex-shrink: 0;
78
+ }
79
+
80
+ .fileListItem__removeButton {
81
+ font-size: 16px;
82
+ width: 1.75rem;
83
+ height: 1.75rem;
84
+ border-radius: var(--border-radius-full);
85
+ background-color: transparent;
86
+ border: none;
87
+ display: flex;
88
+ color: var(--colour-branding-main);
89
+ align-items: center;
90
+ justify-content: center;
91
+ padding: 0;
92
+ min-height: auto;
93
+ transition: all var(--transition-base) ease-in-out;
94
+ }
95
+
96
+ .fileListItem__removeButton:hover:not(:disabled) {
97
+ color: var(--colour-branding-light);
98
+ }
99
+
100
+ .fileListItem__removeButton:disabled {
101
+ opacity: 0.5;
102
+ cursor: not-allowed;
103
+ }
104
+
105
+ /* Add more files button */
106
+ .listingFileInput__addMore {
107
+ margin-top: 0.75rem;
108
+ text-align: center;
109
+ }
110
+
111
+ .listingFileInput__addMoreButton {
112
+ font-size: var(--font-size-sm);
113
+ padding: 0.5rem 1rem;
114
+ }
115
+
116
+ /* Upload progress styling */
117
+ .fileListItem--uploading {
118
+ background-color: var(--colour-branding-secondary-light);
119
+ border-color: var(--colour-branding-action);
120
+ }
121
+
122
+ .fileListItem__progress {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 0.5rem;
126
+ font-size: var(--font-size-xs);
127
+ color: var(--colour-branding-action);
128
+ }
129
+
130
+ .fileListItem__spinner {
131
+ color: var(--colour-branding-action);
132
+ animation: spin 1s linear infinite;
133
+ }
134
+
135
+ @keyframes spin {
136
+ 0% {
137
+ transform: rotate(0deg);
138
+ }
139
+ 100% {
140
+ transform: rotate(360deg);
141
+ }
142
+ }
143
+
144
+ /* Responsive design */
145
+ @media (max-width: 768px) {
146
+ .fileListItem {
147
+ flex-direction: column;
148
+ align-items: stretch;
149
+ gap: 0.5rem;
150
+ }
151
+
152
+ .fileListItem__icon {
153
+ text-align: center;
154
+ }
155
+
156
+ .fileListItem__actions {
157
+ text-align: center;
158
+ }
159
+ }
160
+
161
+ /* Integration with existing field styling */
162
+ .listingFileInput__dropZone .imageInput_upload {
163
+ background-color: var(--bg-white);
164
+ transition: all var(--transition-base) ease-in-out;
165
+ }
166
+
167
+ .listingFileInput__dropZone .imageInput_upload:hover {
168
+ background-color: var(--bg-bluegrey);
169
+ }
170
+
171
+ .listingFileInput__dropZone .imageInput_dropZoneActive {
172
+ background-color: var(--bg-bluegrey);
173
+ }
174
+
175
+ .listingFileInput__dropZone .imageInput_icon {
176
+ width: 1.5rem;
177
+ height: 2rem;
178
+ margin: 0 auto;
179
+ display: block;
180
+ }
181
+
182
+ .listingFileInput__dropZone .imageInput_helpText {
183
+ font-size: var(--font-size-base);
184
+ color: var(--colour-lightgrey);
185
+ margin-top: 1.25rem;
186
+ text-align: center;
187
+ }
188
+
189
+ .listingFileInput__dropZone .imageInput_button {
190
+ text-align: center;
191
+ margin-top: 1rem;
192
+ }
@@ -0,0 +1,90 @@
1
+ import React, { useState } from "react";
2
+ import { PlussCore } from "../../feature.config";
3
+ const { Components } = PlussCore;
4
+ const { Text, Button } = Components;
5
+ import ListingField from "./ListingField.jsx";
6
+ import styles from "./ListingForm.module.css";
7
+ import formStyles from "../../screens/Form.module.css";
8
+
9
+ const ListingForm = ({
10
+ featureDefinition,
11
+ onSubmit,
12
+ mode = "create",
13
+ initialData,
14
+ onChange,
15
+ disabled = false,
16
+ hideSubmitButton = false,
17
+ formData: externalFormData,
18
+ setFormData: externalSetFormData,
19
+ formErrors = {},
20
+ showErrors = false,
21
+ formErrorMessage = null, // For form-level validation errors like "Form data is invalid"
22
+ }) => {
23
+ // Use external state if provided, otherwise use internal state (backward compatibility)
24
+ const [internalFormData, setInternalFormData] = useState(
25
+ initialData?.fields || {},
26
+ );
27
+ const formData =
28
+ externalFormData !== undefined ? externalFormData : internalFormData;
29
+ const setFormData = externalSetFormData || setInternalFormData;
30
+
31
+ React.useEffect(() => {
32
+ // Only update if using internal state
33
+ if (!externalSetFormData) {
34
+ setFormData(initialData?.fields || {});
35
+ }
36
+ }, [initialData, externalSetFormData]);
37
+
38
+ const handleFieldChange = (fieldId, value) => {
39
+ const newFormData = { ...formData, [fieldId]: value };
40
+
41
+ setFormData(newFormData);
42
+ if (onChange) {
43
+ onChange();
44
+ }
45
+ };
46
+
47
+ return (
48
+ <div className={styles.root}>
49
+ {/* Form-level error message */}
50
+ {formErrorMessage && (
51
+ <div className={formStyles.errorMessageContainer}>
52
+ <Text className={formStyles.errorMessage}>{formErrorMessage}</Text>
53
+ </div>
54
+ )}
55
+
56
+ {featureDefinition.fields
57
+ .slice()
58
+ .sort((a, b) => a.order - b.order)
59
+ .map((field) => (
60
+ <ListingField
61
+ key={field.id}
62
+ field={field}
63
+ value={formData[field.id]}
64
+ onChange={handleFieldChange}
65
+ showError={showErrors && formErrors[field.id]}
66
+ errorMessage={formErrors[field.id]}
67
+ isActive={showErrors && formErrors[field.id]}
68
+ disabled={disabled}
69
+ />
70
+ ))}
71
+
72
+ {!hideSubmitButton && (
73
+ <Button
74
+ onClick={() => onSubmit({ fields: formData })}
75
+ disabled={disabled}
76
+ isActive={true}
77
+ buttonType="primary"
78
+ >
79
+ {disabled
80
+ ? "Saving..."
81
+ : mode === "create"
82
+ ? "Create Listing"
83
+ : "Update Listing"}
84
+ </Button>
85
+ )}
86
+ </div>
87
+ );
88
+ };
89
+
90
+ export default ListingForm;
@@ -0,0 +1,38 @@
1
+ /* ListingForm CSS Module */
2
+
3
+ /* Block */
4
+ .root {
5
+ display: flex;
6
+ flex-direction: column;
7
+ background-color: var(--bg-white);
8
+ gap: 1.25rem;
9
+ }
10
+
11
+ .button {
12
+ background-color: var(--colour-blue);
13
+ color: var(--bg-white);
14
+ border: none;
15
+ padding: 1rem 1.5rem;
16
+ border-radius: var(--border-radius-md);
17
+ cursor: pointer;
18
+ font-size: var(--font-size-base);
19
+ font-weight: var(--font-weight-semibold);
20
+ transition:
21
+ background-color var(--transition-base) ease,
22
+ transform var(--transition-fast) ease;
23
+ min-height: 3rem;
24
+ box-shadow: 0 2px 4px var(--text-dark-alpha20);
25
+ }
26
+
27
+ .button:hover:not(.button--disabled) {
28
+ background-color: var(--colour-darkblue);
29
+ transform: translateY(-1px);
30
+ box-shadow: 0 4px 8px var(--text-dark-alpha20);
31
+ }
32
+
33
+ .button--disabled {
34
+ background-color: var(--linegrey);
35
+ cursor: not-allowed;
36
+ transform: none;
37
+ box-shadow: none;
38
+ }
@@ -0,0 +1,236 @@
1
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { ImageInput, Text } from "../../components";
4
+ import { iconImports } from "../../components/iconImports.js";
5
+ import styles from "./ListingField.module.css";
6
+ import galleryStyles from "./ListingGalleryInput.module.css";
7
+
8
+ const ListingGalleryInput = ({
9
+ field,
10
+ value,
11
+ onChange,
12
+ showError,
13
+ errorMessage,
14
+ disabled = false,
15
+ }) => {
16
+ const imageInputRef = useRef(null);
17
+ const [images, setImages] = useState([]);
18
+ const [validationErrors, setValidationErrors] = useState([]);
19
+
20
+ // Initialize images from value prop
21
+ useEffect(() => {
22
+ if (Array.isArray(value)) {
23
+ setImages(value);
24
+ } else if (value && typeof value === "string") {
25
+ // Handle legacy single image string
26
+ setImages([value]);
27
+ } else {
28
+ setImages([]);
29
+ }
30
+ }, [value]);
31
+
32
+ const handleGalleryChange = (imageData) => {
33
+ let newImages = [];
34
+
35
+ // Handle different data structures from ImageInput
36
+ if (Array.isArray(imageData)) {
37
+ newImages = imageData
38
+ .map((img) => {
39
+ if (typeof img === "object" && img !== null) {
40
+ return img.url || img.src || img.imageUrl || JSON.stringify(img);
41
+ }
42
+ return img;
43
+ })
44
+ .filter((img) => img && img !== ""); // Remove empty values
45
+ } else if (typeof imageData === "object" && imageData !== null) {
46
+ newImages = [
47
+ imageData.url ||
48
+ imageData.src ||
49
+ imageData.imageUrl ||
50
+ JSON.stringify(imageData),
51
+ ];
52
+ } else if (imageData && imageData !== "") {
53
+ newImages = [imageData];
54
+ }
55
+
56
+ setImages(newImages);
57
+
58
+ // Validate images
59
+ const errors = validateGallery(newImages);
60
+ setValidationErrors(errors);
61
+
62
+ // Pass array to parent
63
+ onChange(field.id, newImages);
64
+ };
65
+
66
+ const validateGallery = (imageArray) => {
67
+ const errors = [];
68
+ const { values } = field;
69
+
70
+ // Required field validation
71
+ if (values.isRequired && (!imageArray || imageArray.length === 0)) {
72
+ errors.push(`${values.label} is required`);
73
+ }
74
+
75
+ // Minimum images validation
76
+ if (values.minImages && imageArray.length < values.minImages) {
77
+ errors.push(
78
+ `Minimum ${values.minImages} image${values.minImages > 1 ? "s" : ""} required`,
79
+ );
80
+ }
81
+
82
+ // Maximum images validation
83
+ if (values.maxImages && imageArray.length > values.maxImages) {
84
+ errors.push(
85
+ `Maximum ${values.maxImages} image${values.maxImages > 1 ? "s" : ""} allowed`,
86
+ );
87
+ }
88
+
89
+ return errors;
90
+ };
91
+
92
+ const hasError = () => {
93
+ if (showError) return true;
94
+ return validationErrors.length > 0;
95
+ };
96
+
97
+ const getErrorMessage = () => {
98
+ if (errorMessage) return errorMessage;
99
+ if (validationErrors.length > 0) {
100
+ return validationErrors[0]; // Show first validation error
101
+ }
102
+ return `${field.values.label} is required`;
103
+ };
104
+
105
+ const getImageCountText = () => {
106
+ const { minImages, maxImages } = field.values;
107
+ const count = images.length;
108
+
109
+ if (minImages && maxImages) {
110
+ return `${count} / ${minImages}-${maxImages} images`;
111
+ } else if (minImages) {
112
+ return `${count} / ${minImages}+ images`;
113
+ } else if (maxImages) {
114
+ return `${count} / ${maxImages} images`;
115
+ }
116
+ return `${count} images`;
117
+ };
118
+
119
+ const removeImage = (indexToRemove) => {
120
+ const newImages = images.filter((_, index) => index !== indexToRemove);
121
+ setImages(newImages);
122
+
123
+ const errors = validateGallery(newImages);
124
+ setValidationErrors(errors);
125
+
126
+ onChange(field.id, newImages);
127
+ };
128
+
129
+ return (
130
+ <div>
131
+ {/* Field header with icon and description */}
132
+ <div className={styles.listingField__header}>
133
+ <div className={styles.listingField__icon}>
134
+ <FontAwesomeIcon icon={iconImports.image} />
135
+ </div>
136
+ <div className={styles.listingField__content}>
137
+ <h3 className={styles.listingField__title}>Image Gallery</h3>
138
+ <p className={styles.listingField__description}>
139
+ Upload multiple photos or images to create a gallery
140
+ </p>
141
+ </div>
142
+ </div>
143
+
144
+ <Text type="formLabel" className={galleryStyles.formLabel}>
145
+ {field.values.label}
146
+ {field.values.isRequired && (
147
+ <span className={styles.requiredAsterisk}>*</span>
148
+ )}
149
+ </Text>
150
+
151
+ {/* Image count indicator */}
152
+ <Text type="help" className={galleryStyles.imageCountText}>
153
+ {getImageCountText()}
154
+ </Text>
155
+
156
+ <div className={galleryStyles.galleryContainer}>
157
+ <div className={hasError() ? galleryStyles.galleryErrorWrapper : ""}>
158
+ <ImageInput
159
+ ref={imageInputRef}
160
+ containerStyle={{ width: "100%", height: 200 }}
161
+ className="imageInputOuter-grid"
162
+ refreshCallback={handleGalleryChange}
163
+ hasDefault={null}
164
+ multiple={true}
165
+ limit={field.values.maxImages}
166
+ disabled={disabled}
167
+ noCompress={false}
168
+ grid={true}
169
+ simpleStyle={true}
170
+ />
171
+ {/* ImageInput expects a string URL for hasDefault, but we have an array.
172
+ For now, passing null to prevent url.split error */}
173
+ </div>
174
+
175
+ {/* Image preview grid */}
176
+ {images.length > 0 && (
177
+ <div className={galleryStyles.imagePreviewContainer}>
178
+ <Text type="formLabel" className={galleryStyles.formLabel}>
179
+ Current Images ({images.length})
180
+ </Text>
181
+ <div
182
+ className={galleryStyles.imageGrid}
183
+ style={{
184
+ "--columns": Math.min(images.length, 4),
185
+ }}
186
+ >
187
+ {images.map((image, index) => {
188
+ return (
189
+ <div key={`image-${index}`} className={galleryStyles.imageItem}>
190
+ <img
191
+ src={image}
192
+ alt={`Gallery item ${index + 1}`}
193
+ className={galleryStyles.imageThumbnail}
194
+ />
195
+ {!disabled && (
196
+ <button
197
+ type="button"
198
+ onClick={() => removeImage(index)}
199
+ className={galleryStyles.removeImageButton}
200
+ >
201
+ <FontAwesomeIcon icon={iconImports.minusCircle} />
202
+ </button>
203
+ )}
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ </div>
209
+ )}
210
+
211
+ {hasError() && (
212
+ <Text type="help" className={galleryStyles.errorMessage}>
213
+ {getErrorMessage()}
214
+ </Text>
215
+ )}
216
+ </div>
217
+
218
+ {field.values.helpText && (
219
+ <Text type="help" className={galleryStyles.helpText}>
220
+ {field.values.helpText}
221
+ </Text>
222
+ )}
223
+
224
+ {/* Validation hints */}
225
+ {field.values.minImages || field.values.maxImages ? (
226
+ <Text type="help" className={galleryStyles.validationHint}>
227
+ {field.values.minImages && `Min: ${field.values.minImages} images`}
228
+ {field.values.minImages && field.values.maxImages && " | "}
229
+ {field.values.maxImages && `Max: ${field.values.maxImages} images`}
230
+ </Text>
231
+ ) : null}
232
+ </div>
233
+ );
234
+ };
235
+
236
+ export default ListingGalleryInput;
@@ -0,0 +1,131 @@
1
+ /* ListingGalleryInput BEM CSS Module */
2
+
3
+ /* Gallery container styles */
4
+ .galleryContainer {
5
+ margin-top: 0.75rem;
6
+ }
7
+
8
+ /* Error border wrapper */
9
+ .galleryErrorWrapper {
10
+ border: 1px solid var(--colour-error);
11
+ border-radius: var(--border-radius-base);
12
+ padding: 0.25rem;
13
+ background-color: var(--colour-error-light, rgba(220, 53, 69, 0.05));
14
+ }
15
+
16
+ /* Image preview grid container */
17
+ .imagePreviewContainer {
18
+ margin-top: 1rem;
19
+ }
20
+
21
+ .imageGrid {
22
+ display: grid;
23
+ grid-template-columns: repeat(var(--columns, 4), 1fr);
24
+ gap: 1rem;
25
+ margin-top: 0.5rem;
26
+ }
27
+
28
+ /* Responsive grid for different screen sizes */
29
+ @media (max-width: 768px) {
30
+ .imageGrid {
31
+ grid-template-columns: repeat(3, 1fr);
32
+ gap: 0.75rem;
33
+ }
34
+ }
35
+
36
+ @media (max-width: 480px) {
37
+ .imageGrid {
38
+ grid-template-columns: repeat(2, 1fr);
39
+ gap: 0.5rem;
40
+ }
41
+ }
42
+
43
+ /* Individual image item */
44
+ .imageItem {
45
+ position: relative;
46
+ border-radius: var(--border-radius-lg);
47
+ overflow: hidden;
48
+ background-color: var(--bg-bluegrey);
49
+ transition:
50
+ transform var(--transition-base) ease,
51
+ box-shadow var(--transition-base) ease;
52
+ }
53
+
54
+ .imageItem:hover {
55
+ transform: translateY(-2px);
56
+ box-shadow: var(--shadow-lg);
57
+ }
58
+
59
+ .imageThumbnail {
60
+ width: 100%;
61
+ height: 100px;
62
+ object-fit: cover;
63
+ display: block;
64
+ aspect-ratio: 1;
65
+ }
66
+
67
+ /* Remove image button */
68
+ .removeImageButton {
69
+ position: absolute;
70
+ top: 0.5rem;
71
+ right: 0.5rem;
72
+ background: transparent;
73
+ color: var(--colour-branding-primary);
74
+ border: none;
75
+ border-radius: var(--border-radius-full);
76
+ width: 1.75rem;
77
+ height: 1.75rem;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ cursor: pointer;
82
+ opacity: 0;
83
+ transition: opacity var(--transition-base) ease;
84
+ }
85
+
86
+ .imageItem:hover .removeImageButton {
87
+ opacity: 1;
88
+ }
89
+
90
+ .removeImageButton:hover:not(:disabled) {
91
+ color: var(--colour-branding-light);
92
+ }
93
+
94
+ .removeImageButton:disabled {
95
+ opacity: 0.5;
96
+ cursor: not-allowed;
97
+ }
98
+
99
+ /* Error message styling */
100
+ .errorMessage {
101
+ color: var(--colour-red);
102
+ margin-top: 0.5rem;
103
+ display: block;
104
+ }
105
+
106
+ /* Help text styling */
107
+ .helpText {
108
+ color: var(--text-bluegrey);
109
+ margin-top: 0.5rem;
110
+ display: block;
111
+ }
112
+
113
+ /* Validation hints */
114
+ .validationHint {
115
+ color: var(--text-bluegrey);
116
+ margin-top: 0.25rem;
117
+ display: block;
118
+ font-size: var(--font-size-xxs);
119
+ }
120
+
121
+ /* Form label styling */
122
+ .formLabel {
123
+ display: block;
124
+ margin-bottom: 8px;
125
+ }
126
+
127
+ /* Help text for image count */
128
+ .imageCountText {
129
+ margin-bottom: 0.5rem;
130
+ display: block;
131
+ }