@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.
- 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,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
3
|
+
import { GenericInput, Text } from "../index.js";
|
|
4
|
+
import { iconImports } from "../iconImports.js";
|
|
5
|
+
import styles from "./ListingField.module.css";
|
|
6
|
+
|
|
7
|
+
const ListingDescriptionInput = ({
|
|
8
|
+
field,
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
showError,
|
|
12
|
+
errorMessage,
|
|
13
|
+
}) => {
|
|
14
|
+
const { label, placeholder, isRequired, helpText } = field.values;
|
|
15
|
+
|
|
16
|
+
const handleChange = (e) => {
|
|
17
|
+
onChange(field.id, e.target.value);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isValid = () => {
|
|
21
|
+
if (isRequired) {
|
|
22
|
+
const isEmpty =
|
|
23
|
+
!value || (typeof value === "string" && value.trim() === "");
|
|
24
|
+
return !isEmpty;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const displayLabel = (
|
|
30
|
+
<>
|
|
31
|
+
{label}
|
|
32
|
+
{isRequired && <span className={styles.requiredAsterisk}>*</span>}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
{/* Field header with icon and description */}
|
|
39
|
+
<div className={styles.listingField__header}>
|
|
40
|
+
<div className={styles.listingField__icon}>
|
|
41
|
+
<FontAwesomeIcon icon={iconImports.alignLeft} />
|
|
42
|
+
</div>
|
|
43
|
+
<div className={styles.listingField__content}>
|
|
44
|
+
<h3 className={styles.listingField__title}>Description</h3>
|
|
45
|
+
<p className={styles.listingField__description}>
|
|
46
|
+
Longer text area for detailed information or stories
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<GenericInput
|
|
52
|
+
type="textarea"
|
|
53
|
+
label={displayLabel}
|
|
54
|
+
placeholder={placeholder || ""}
|
|
55
|
+
value={value || ""}
|
|
56
|
+
onChange={handleChange}
|
|
57
|
+
isRequired={isRequired}
|
|
58
|
+
showError={!!showError}
|
|
59
|
+
errorMessage={errorMessage}
|
|
60
|
+
isValid={isValid()}
|
|
61
|
+
alwaysShowLabel
|
|
62
|
+
style={{ resize: "vertical" }}
|
|
63
|
+
/>
|
|
64
|
+
{helpText && (
|
|
65
|
+
<div style={{ marginTop: "8px", color: "#6c7a90", fontSize: "14px" }}>
|
|
66
|
+
{helpText}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default ListingDescriptionInput;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ListingTextInput from "./ListingTextInput.jsx";
|
|
3
|
+
import ListingDescriptionInput from "./ListingDescriptionInput.jsx";
|
|
4
|
+
import ListingImageInput from "./ListingImageInput.jsx";
|
|
5
|
+
import ListingGalleryInput from "./ListingGalleryInput.jsx";
|
|
6
|
+
import ListingCTAInput from "./ListingCTAInput.jsx";
|
|
7
|
+
import ListingFileInput from "./ListingFileInput.jsx";
|
|
8
|
+
import styles from "./ListingField.module.css";
|
|
9
|
+
|
|
10
|
+
const ListingField = ({
|
|
11
|
+
field,
|
|
12
|
+
value,
|
|
13
|
+
onChange,
|
|
14
|
+
showError,
|
|
15
|
+
errorMessage,
|
|
16
|
+
isActive,
|
|
17
|
+
}) => {
|
|
18
|
+
const fieldClassName =
|
|
19
|
+
showError || isActive
|
|
20
|
+
? `${styles.listingField} ${styles.hasError}`
|
|
21
|
+
: styles.listingField;
|
|
22
|
+
|
|
23
|
+
const renderField = () => {
|
|
24
|
+
switch (field.type) {
|
|
25
|
+
case "title":
|
|
26
|
+
return (
|
|
27
|
+
<ListingTextInput
|
|
28
|
+
field={field}
|
|
29
|
+
value={value}
|
|
30
|
+
onChange={onChange}
|
|
31
|
+
showError={showError}
|
|
32
|
+
errorMessage={errorMessage}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
case "description":
|
|
36
|
+
return (
|
|
37
|
+
<ListingDescriptionInput
|
|
38
|
+
field={field}
|
|
39
|
+
value={value}
|
|
40
|
+
onChange={onChange}
|
|
41
|
+
showError={showError}
|
|
42
|
+
errorMessage={errorMessage}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
case "image":
|
|
46
|
+
case "feature-image":
|
|
47
|
+
return (
|
|
48
|
+
<ListingImageInput
|
|
49
|
+
field={field}
|
|
50
|
+
value={value}
|
|
51
|
+
onChange={onChange}
|
|
52
|
+
showError={showError}
|
|
53
|
+
errorMessage={errorMessage}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
case "gallery":
|
|
57
|
+
return (
|
|
58
|
+
<ListingGalleryInput
|
|
59
|
+
field={field}
|
|
60
|
+
value={value}
|
|
61
|
+
onChange={onChange}
|
|
62
|
+
showError={showError}
|
|
63
|
+
errorMessage={errorMessage}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
case "cta":
|
|
67
|
+
return (
|
|
68
|
+
<ListingCTAInput
|
|
69
|
+
field={field}
|
|
70
|
+
value={value}
|
|
71
|
+
onChange={onChange}
|
|
72
|
+
showError={showError}
|
|
73
|
+
errorMessage={errorMessage}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
case "file":
|
|
77
|
+
return (
|
|
78
|
+
<ListingFileInput
|
|
79
|
+
field={field}
|
|
80
|
+
value={value}
|
|
81
|
+
onChange={onChange}
|
|
82
|
+
showError={showError}
|
|
83
|
+
errorMessage={errorMessage}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
default:
|
|
87
|
+
return <div>Field type {field.type} not implemented</div>;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={fieldClassName} data-field-id={field.id}>
|
|
93
|
+
{renderField()}
|
|
94
|
+
{showError && errorMessage && (
|
|
95
|
+
<div className={styles.listingField__error}>{errorMessage}</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default ListingField;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* Simple listing field styling - clean vertical layout */
|
|
2
|
+
|
|
3
|
+
.listingField {
|
|
4
|
+
padding: 0.5rem 0.8rem 0.6rem;
|
|
5
|
+
border-radius: var(--border-radius-xl);
|
|
6
|
+
background: var(--bg-white);
|
|
7
|
+
transition: box-shadow var(--transition-base) ease;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Field header with icon and description */
|
|
11
|
+
.listingField__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
display: none;
|
|
14
|
+
align-items: flex-start;
|
|
15
|
+
gap: 0.3rem;
|
|
16
|
+
|
|
17
|
+
padding: 0 0.6rem 0.3rem;
|
|
18
|
+
border-bottom: 1px solid var(--border-line-grey);
|
|
19
|
+
margin: 0 calc(-1 * 0.7rem);
|
|
20
|
+
margin-bottom: 0.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.listingField__icon {
|
|
24
|
+
color: var(--text-bluegrey);
|
|
25
|
+
font-size: var(--font-size-base);
|
|
26
|
+
flex-shrink: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.listingField__content {
|
|
30
|
+
flex: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.listingField__title {
|
|
34
|
+
margin: 0 0 0.1rem 0;
|
|
35
|
+
font-weight: var(--font-weight-semibold);
|
|
36
|
+
font-size: var(--font-size-base);
|
|
37
|
+
color: var(--text-dark);
|
|
38
|
+
line-height: var(--line-height-snug);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.listingField__description {
|
|
42
|
+
margin: 0;
|
|
43
|
+
font-size: var(--font-size-xs);
|
|
44
|
+
color: var(--text-bluegrey);
|
|
45
|
+
line-height: var(--line-height-relaxed);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.listingField:last-child {
|
|
49
|
+
margin-bottom: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Error state styling */
|
|
53
|
+
.listingField--error {
|
|
54
|
+
border-color: var(--colour-red);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Required field asterisk */
|
|
58
|
+
.requiredAsterisk {
|
|
59
|
+
color: var(--colour-red);
|
|
60
|
+
margin-left: 0.1rem;
|
|
61
|
+
font-weight: var(--font-weight-bold);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Error message styling */
|
|
65
|
+
.listingField__error {
|
|
66
|
+
margin-top: 0.1rem;
|
|
67
|
+
color: var(--colour-red);
|
|
68
|
+
font-size: var(--font-size-xs);
|
|
69
|
+
font-weight: var(--font-weight-normal);
|
|
70
|
+
line-height: var(--line-height-relaxed);
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 0.1rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.listingField__error::before {
|
|
77
|
+
content: "⚠";
|
|
78
|
+
font-size: var(--font-size-sm);
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Make error more visible */
|
|
83
|
+
.listingField.hasError {
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.listingField.hasError input,
|
|
88
|
+
.listingField.hasError textarea,
|
|
89
|
+
.listingField.hasError select,
|
|
90
|
+
.listingField.hasError .genericInput,
|
|
91
|
+
.listingField.hasError .genericInput input,
|
|
92
|
+
.listingField.hasError .genericInput textarea,
|
|
93
|
+
.listingField.hasError .imageInputOuter-single,
|
|
94
|
+
.listingField.hasError .imageInputOuter-grid,
|
|
95
|
+
.listingField.hasError .FileInput,
|
|
96
|
+
.listingField.hasError .imageInput,
|
|
97
|
+
.listingField.hasError input[type="file"],
|
|
98
|
+
.listingField.hasError .file-input,
|
|
99
|
+
.listingField.hasError div[class*="imageInput"],
|
|
100
|
+
.listingField.hasError div[class*="file-input"],
|
|
101
|
+
.listingField.hasError .image-container,
|
|
102
|
+
.listingField.hasError .image-grid,
|
|
103
|
+
.listingField.hasError img {
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Hover effect for better interactivity */
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from "react";
|
|
2
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
3
|
+
import { FileInput, Text, Button } from "../../components";
|
|
4
|
+
import { iconImports } from "../../components/iconImports.js";
|
|
5
|
+
import FileListItem from "./FileListItem.jsx";
|
|
6
|
+
import listingFieldStyles from "./ListingField.module.css";
|
|
7
|
+
import styles from "./ListingFileInput.module.css";
|
|
8
|
+
|
|
9
|
+
const ListingFileInput = ({
|
|
10
|
+
field,
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
showError,
|
|
14
|
+
errorMessage,
|
|
15
|
+
disabled = false,
|
|
16
|
+
}) => {
|
|
17
|
+
const fileInputRef = useRef(null);
|
|
18
|
+
const [internalFiles, setInternalFiles] = useState([]);
|
|
19
|
+
|
|
20
|
+
// Normalize file data to enhanced structure - always handle as multiple files
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (value) {
|
|
23
|
+
// Handle multiple files - could be array of URLs or array of file objects
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
const normalizedFiles = value
|
|
26
|
+
.map((file) => {
|
|
27
|
+
if (typeof file === "string") {
|
|
28
|
+
// Backward compatibility: array of URLs
|
|
29
|
+
const fileName = file.split("/").pop() || "Unknown File";
|
|
30
|
+
return {
|
|
31
|
+
url: file,
|
|
32
|
+
name: fileName.replace(/\.[^/.]+$/, ""), // Remove extension
|
|
33
|
+
originalName: fileName,
|
|
34
|
+
uploading: false,
|
|
35
|
+
};
|
|
36
|
+
} else if (file && typeof file === "object" && file.url) {
|
|
37
|
+
// Enhanced structure
|
|
38
|
+
return {
|
|
39
|
+
url: file.url,
|
|
40
|
+
name: file.name || file.originalName || "Unknown File",
|
|
41
|
+
originalName:
|
|
42
|
+
file.originalName ||
|
|
43
|
+
file.url.split("/").pop() ||
|
|
44
|
+
"Unknown File",
|
|
45
|
+
uploading: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
setInternalFiles(normalizedFiles);
|
|
52
|
+
} else {
|
|
53
|
+
// Handle single file values - normalize to array
|
|
54
|
+
let singleFile = null;
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
// Backward compatibility: single URL
|
|
57
|
+
const fileName = value.split("/").pop() || "Unknown File";
|
|
58
|
+
singleFile = {
|
|
59
|
+
url: value,
|
|
60
|
+
name: fileName.replace(/\.[^/.]+$/, ""),
|
|
61
|
+
originalName: fileName,
|
|
62
|
+
uploading: false,
|
|
63
|
+
};
|
|
64
|
+
} else if (value && typeof value === "object" && value.url) {
|
|
65
|
+
// Enhanced structure - preserve empty name if explicitly set
|
|
66
|
+
singleFile = {
|
|
67
|
+
url: value.url,
|
|
68
|
+
name:
|
|
69
|
+
value.name !== undefined
|
|
70
|
+
? value.name
|
|
71
|
+
: value.originalName || "Unknown File",
|
|
72
|
+
originalName:
|
|
73
|
+
value.originalName ||
|
|
74
|
+
value.url.split("/").pop() ||
|
|
75
|
+
"Unknown File",
|
|
76
|
+
uploading: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
setInternalFiles(singleFile ? [singleFile] : []);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
setInternalFiles([]);
|
|
83
|
+
}
|
|
84
|
+
}, [value]);
|
|
85
|
+
|
|
86
|
+
// Handle new file upload from FileInput component
|
|
87
|
+
const handleFileUpload = (uploadedUrl) => {
|
|
88
|
+
// Handle case where uploadedUrl might be undefined
|
|
89
|
+
if (!uploadedUrl) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fileName = uploadedUrl.split("/").pop() || "Unknown File";
|
|
94
|
+
const newFile = {
|
|
95
|
+
url: uploadedUrl,
|
|
96
|
+
name: "", // Start with empty name as requested
|
|
97
|
+
originalName: fileName,
|
|
98
|
+
uploading: false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Always add new file to existing array for multiple file support
|
|
102
|
+
const updatedFiles = [...internalFiles, newFile];
|
|
103
|
+
setInternalFiles(updatedFiles);
|
|
104
|
+
notifyParent(updatedFiles);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Handle file name change
|
|
108
|
+
const handleNameChange = (url, newName) => {
|
|
109
|
+
const updatedFiles = internalFiles.map((file) =>
|
|
110
|
+
file.url === url ? { ...file, name: newName } : file,
|
|
111
|
+
);
|
|
112
|
+
setInternalFiles(updatedFiles);
|
|
113
|
+
notifyParent(updatedFiles);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Handle file removal
|
|
117
|
+
const handleFileRemove = (url) => {
|
|
118
|
+
const updatedFiles = internalFiles.filter((file) => file.url !== url);
|
|
119
|
+
setInternalFiles(updatedFiles);
|
|
120
|
+
notifyParent(updatedFiles);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Notify parent component of changes
|
|
124
|
+
const notifyParent = (files) => {
|
|
125
|
+
if (onChange) {
|
|
126
|
+
// Always return array of files for multiple file support
|
|
127
|
+
onChange(field.id, files);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Define acceptable file types for documents and common formats
|
|
132
|
+
const acceptTypes = {
|
|
133
|
+
"application/pdf": [".pdf"],
|
|
134
|
+
"application/msword": [".doc"],
|
|
135
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
|
|
136
|
+
".docx",
|
|
137
|
+
],
|
|
138
|
+
"application/vnd.ms-excel": [".xls"],
|
|
139
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [
|
|
140
|
+
".xlsx",
|
|
141
|
+
],
|
|
142
|
+
"text/plain": [".txt"],
|
|
143
|
+
"text/csv": [".csv"],
|
|
144
|
+
"image/jpeg": [".jpg", ".jpeg"],
|
|
145
|
+
"image/png": [".png"],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Always show drop zone regardless of existing files
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className={styles.listingFileInput}>
|
|
152
|
+
{/* Field header with icon and description */}
|
|
153
|
+
<div className={listingFieldStyles.listingField__header}>
|
|
154
|
+
<div className={listingFieldStyles.listingField__icon}>
|
|
155
|
+
<FontAwesomeIcon icon={iconImports.file} />
|
|
156
|
+
</div>
|
|
157
|
+
<div className={listingFieldStyles.listingField__content}>
|
|
158
|
+
<h3 className={listingFieldStyles.listingField__title}>File</h3>
|
|
159
|
+
<p className={listingFieldStyles.listingField__description}>
|
|
160
|
+
Attach documents, PDFs, or other files
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<Text type="formLabel" style={{ display: "block", marginBottom: "8px" }}>
|
|
166
|
+
{field.values.label}
|
|
167
|
+
{field.values.isRequired && (
|
|
168
|
+
<span className={listingFieldStyles.requiredAsterisk}>*</span>
|
|
169
|
+
)}
|
|
170
|
+
</Text>
|
|
171
|
+
|
|
172
|
+
{/* Drop zone */}
|
|
173
|
+
<div
|
|
174
|
+
className={`${styles.listingFileInput__dropZone} ${showError ? styles["listingFileInput__dropZone--error"] : ""}`}
|
|
175
|
+
>
|
|
176
|
+
<FileInput
|
|
177
|
+
ref={fileInputRef}
|
|
178
|
+
className="imageInputOuter-single"
|
|
179
|
+
refreshCallback={handleFileUpload}
|
|
180
|
+
hasDefault={null}
|
|
181
|
+
accept={acceptTypes}
|
|
182
|
+
multiple={false} // Always handle one file at a time in the upload callback
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* File list */}
|
|
188
|
+
{internalFiles.length > 0 && (
|
|
189
|
+
<div className={styles.listingFileInput__fileList}>
|
|
190
|
+
{internalFiles.map((file) => (
|
|
191
|
+
<FileListItem
|
|
192
|
+
key={file.url}
|
|
193
|
+
file={file}
|
|
194
|
+
onNameChange={handleNameChange}
|
|
195
|
+
onRemove={handleFileRemove}
|
|
196
|
+
disabled={disabled}
|
|
197
|
+
/>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Add more files button - always show when files exist */}
|
|
203
|
+
{internalFiles.length > 0 && (
|
|
204
|
+
<div>
|
|
205
|
+
<FileInput
|
|
206
|
+
ref={fileInputRef}
|
|
207
|
+
className="imageInputOuter-single"
|
|
208
|
+
refreshCallback={handleFileUpload}
|
|
209
|
+
hasDefault={null}
|
|
210
|
+
accept={acceptTypes}
|
|
211
|
+
multiple={false}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
customButton={
|
|
214
|
+
<Button
|
|
215
|
+
buttonType="secondary"
|
|
216
|
+
className={styles.listingFileInput__addMoreButton}
|
|
217
|
+
disabled={disabled}
|
|
218
|
+
>
|
|
219
|
+
<FontAwesomeIcon icon={iconImports.plus} />
|
|
220
|
+
Add More Files
|
|
221
|
+
</Button>
|
|
222
|
+
}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{showError && errorMessage && (
|
|
228
|
+
<Text
|
|
229
|
+
type="help"
|
|
230
|
+
style={{
|
|
231
|
+
color: "var(--colour-red)",
|
|
232
|
+
marginTop: "8px",
|
|
233
|
+
display: "block",
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{errorMessage}
|
|
237
|
+
</Text>
|
|
238
|
+
)}
|
|
239
|
+
{field.values.helpText && (
|
|
240
|
+
<Text
|
|
241
|
+
type="help"
|
|
242
|
+
style={{
|
|
243
|
+
color: "var(--text-bluegrey)",
|
|
244
|
+
marginTop: "8px",
|
|
245
|
+
display: "block",
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{field.values.helpText}
|
|
249
|
+
</Text>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export default ListingFileInput;
|