@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.
- package/.babelrc +4 -0
- package/dist/index.cjs.js +7792 -0
- package/package.json +54 -0
- package/rollup.config.js +68 -0
- package/src/actions/featureBuilderStringsActions.js +88 -0
- package/src/actions/featureDefinitionsIndex.js +258 -0
- package/src/actions/formActions.js +311 -0
- package/src/actions/index.js +12 -0
- package/src/actions/listingActions.js +350 -0
- package/src/actions/wizardActions.js +240 -0
- package/src/components/ActivityCardExample.jsx +86 -0
- package/src/components/ActivityCardExample.module.css +130 -0
- package/src/components/BackgroundLoader.jsx +33 -0
- package/src/components/BackgroundLoader.module.css +46 -0
- package/src/components/BaseFieldConfig.jsx +305 -0
- package/src/components/BaseFieldConfig.module.css +42 -0
- package/src/components/CenteredContainer.jsx +29 -0
- package/src/components/CenteredContainer.module.css +171 -0
- package/src/components/DeleteConfirmationPopup.jsx +95 -0
- package/src/components/DeleteConfirmationPopup.module.css +12 -0
- package/src/components/ErrorBoundary.jsx +134 -0
- package/src/components/ErrorBoundary.module.css +77 -0
- package/src/components/ErrorMessage.jsx +85 -0
- package/src/components/ErrorMessage.module.css +116 -0
- package/src/components/ExampleDisplay.jsx +26 -0
- package/src/components/ExampleDisplay.module.css +3 -0
- package/src/components/FeatureBuilderSidebar.jsx +84 -0
- package/src/components/FeatureBuilderSuccessPopup.jsx +55 -0
- package/src/components/FeatureBuilderSuccessPopup.module.css +43 -0
- package/src/components/FeatureBuilderWelcomePopup.jsx +51 -0
- package/src/components/FeatureBuilderWelcomePopup.module.css +21 -0
- package/src/components/FeatureListingCard.jsx +104 -0
- package/src/components/FeatureListingCard.module.css +62 -0
- package/src/components/Fields.jsx +460 -0
- package/src/components/Fields.module.css +159 -0
- package/src/components/IconLoader.jsx +153 -0
- package/src/components/IconLoader.module.css +92 -0
- package/src/components/IconSelector.jsx +112 -0
- package/src/components/IconSelector.module.css +197 -0
- package/src/components/ListingEditor.jsx +406 -0
- package/src/components/ListingEditor.module.css +14 -0
- package/src/components/ListingSuccessPopup.jsx +52 -0
- package/src/components/LoadingScreen.jsx +54 -0
- package/src/components/LoadingScreen.module.css +103 -0
- package/src/components/LoadingState.jsx +40 -0
- package/src/components/LoadingState.module.css +18 -0
- package/src/components/PreviewFull.js +24 -0
- package/src/components/PreviewFull.module.css +11 -0
- package/src/components/PreviewGrid.js +14 -0
- package/src/components/PreviewWidget.js +27 -0
- package/src/components/PreviewWidget.module.css +15 -0
- package/src/components/SidebarLayout.jsx +292 -0
- package/src/components/SidebarLayout.module.css +145 -0
- package/src/components/SkeletonLoader.jsx +128 -0
- package/src/components/SkeletonLoader.module.css +295 -0
- package/src/components/SortButtonGroup.jsx +34 -0
- package/src/components/SortButtonGroup.module.css +51 -0
- package/src/components/ToastContainer.jsx +98 -0
- package/src/components/ToastContainer.module.css +156 -0
- package/src/components/ToggleSwitch.js +40 -0
- package/src/components/ToggleSwitch.module.css +48 -0
- package/src/components/TwoColumnInput.jsx +29 -0
- package/src/components/TwoColumnInput.module.css +32 -0
- package/src/components/ViewFull.js +139 -0
- package/src/components/ViewFull.module.css +71 -0
- package/src/components/ViewWidget.js +62 -0
- package/src/components/ViewWidget.module.css +28 -0
- package/src/components/iconCategories.js +135 -0
- package/src/components/iconImports.js +409 -0
- package/src/components/index.js +61 -0
- package/src/components/listing/FileListItem.jsx +86 -0
- package/src/components/listing/GalleryDisplay.jsx +331 -0
- package/src/components/listing/GalleryDisplay.module.css +309 -0
- package/src/components/listing/ListingCTAInput.jsx +82 -0
- package/src/components/listing/ListingDescriptionInput.jsx +73 -0
- package/src/components/listing/ListingField.jsx +101 -0
- package/src/components/listing/ListingField.module.css +106 -0
- package/src/components/listing/ListingFileInput.jsx +255 -0
- package/src/components/listing/ListingFileInput.module.css +192 -0
- package/src/components/listing/ListingForm.jsx +90 -0
- package/src/components/listing/ListingForm.module.css +38 -0
- package/src/components/listing/ListingGalleryInput.jsx +236 -0
- package/src/components/listing/ListingGalleryInput.module.css +131 -0
- package/src/components/listing/ListingImageInput.jsx +153 -0
- package/src/components/listing/ListingTextInput.jsx +72 -0
- package/src/feature.config.js +130 -0
- package/src/helper/index.js +135 -0
- package/src/hooks/useFeatureDefinitionLoader.js +62 -0
- package/src/images/full.png +0 -0
- package/src/images/fullNoTitle.png +0 -0
- package/src/images/previewWidget.png +0 -0
- package/src/images/widget.png +0 -0
- package/src/index.js +38 -0
- package/src/pages/CreateListingPage.jsx +49 -0
- package/src/pages/EditListingPage.jsx +58 -0
- package/src/reducers/featureBuilderReducer.js +744 -0
- package/src/screens/CreateListing.module.css +45 -0
- package/src/screens/Form.module.css +734 -0
- package/src/screens/FormFieldsStep.jsx +689 -0
- package/src/screens/FormLayoutStep.jsx +445 -0
- package/src/screens/FormOverviewStep.jsx +396 -0
- package/src/screens/ListingScreen.jsx +478 -0
- package/src/screens/ListingScreen.module.css +333 -0
- package/src/selectors/featureBuilderSelectors.js +529 -0
- package/src/types/index.js +91 -0
- package/src/utils/textUtils.js +89 -0
- package/src/validators/galleryValidators.js +345 -0
- package/src/values.config.a.js +49 -0
- package/src/values.config.b.js +49 -0
- package/src/values.config.c.js +49 -0
- package/src/values.config.d.js +49 -0
- package/src/values.config.js +49 -0
- package/src/webapi/featureDefinitionActions.js +0 -0
- package/src/webapi/featuresActions.js +90 -0
- package/src/webapi/helper.js +4 -0
- package/src/webapi/index.js +12 -0
- 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
|
+
}
|