@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,153 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from "react";
|
|
2
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
3
|
+
import { ImageInput, Text, GenericInput } from "../../components";
|
|
4
|
+
import { iconImports } from "../../components/iconImports.js";
|
|
5
|
+
import styles from "./ListingField.module.css";
|
|
6
|
+
|
|
7
|
+
const ListingImageInput = ({
|
|
8
|
+
field,
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
showError,
|
|
12
|
+
errorMessage,
|
|
13
|
+
disabled = false,
|
|
14
|
+
}) => {
|
|
15
|
+
const imageInputRef = useRef(null);
|
|
16
|
+
const [imageUrl, setImageUrl] = useState("");
|
|
17
|
+
const [caption, setCaption] = useState("");
|
|
18
|
+
|
|
19
|
+
// Initialize state from value prop (handle backward compatibility)
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (value) {
|
|
22
|
+
if (typeof value === "object" && value !== null) {
|
|
23
|
+
// New format: { url: "image-url", caption: "caption text" }
|
|
24
|
+
setImageUrl(value.url || "");
|
|
25
|
+
setCaption(value.caption || "");
|
|
26
|
+
} else {
|
|
27
|
+
// Legacy format: simple string URL
|
|
28
|
+
setImageUrl(value);
|
|
29
|
+
setCaption("");
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
setImageUrl("");
|
|
33
|
+
setCaption("");
|
|
34
|
+
}
|
|
35
|
+
}, [value]);
|
|
36
|
+
|
|
37
|
+
const handleImageChange = (imageData) => {
|
|
38
|
+
// Handle different data structures from ImageInput
|
|
39
|
+
let newImageUrl = imageData;
|
|
40
|
+
|
|
41
|
+
// If imageData is an object (from feature library), extract the URL
|
|
42
|
+
if (typeof imageData === "object" && imageData !== null) {
|
|
43
|
+
newImageUrl =
|
|
44
|
+
imageData.url ||
|
|
45
|
+
imageData.src ||
|
|
46
|
+
imageData.imageUrl ||
|
|
47
|
+
JSON.stringify(imageData);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setImageUrl(newImageUrl);
|
|
51
|
+
updateValue(newImageUrl, caption);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleCaptionChange = (newCaption) => {
|
|
55
|
+
setCaption(newCaption);
|
|
56
|
+
updateValue(imageUrl, newCaption);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const updateValue = (url, captionText) => {
|
|
60
|
+
// If captions are enabled, store as object, otherwise as string for backward compatibility
|
|
61
|
+
if (field.values.allowCaption) {
|
|
62
|
+
onChange(field.id, { url, caption: captionText });
|
|
63
|
+
} else {
|
|
64
|
+
onChange(field.id, url);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
{/* Field header with icon and description */}
|
|
71
|
+
<div className={styles.listingField__header}>
|
|
72
|
+
<div className={styles.listingField__icon}>
|
|
73
|
+
<FontAwesomeIcon icon={iconImports.image} />
|
|
74
|
+
</div>
|
|
75
|
+
<div className={styles.listingField__content}>
|
|
76
|
+
<h3 className={styles.listingField__title}>Image</h3>
|
|
77
|
+
<p className={styles.listingField__description}>
|
|
78
|
+
Upload a single photo or image to accompany your content
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<Text type="formLabel" style={{ display: "block", marginBottom: "8px" }}>
|
|
84
|
+
{field.values.label}
|
|
85
|
+
{field.values.isRequired && (
|
|
86
|
+
<span className={styles.requiredAsterisk}>*</span>
|
|
87
|
+
)}
|
|
88
|
+
</Text>
|
|
89
|
+
<div style={{ marginTop: "12px" }}>
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
border: showError ? "1px solid var(--colour-error)" : "none",
|
|
93
|
+
borderRadius: "4px",
|
|
94
|
+
padding: showError ? "0.1rem" : "0",
|
|
95
|
+
backgroundColor: showError
|
|
96
|
+
? "var(--colour-error-light, rgba(220, 53, 69, 0.05))"
|
|
97
|
+
: "transparent",
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<ImageInput
|
|
101
|
+
ref={imageInputRef}
|
|
102
|
+
containerStyle={{ width: 293, height: 168 }}
|
|
103
|
+
className="imageInputOuter-single"
|
|
104
|
+
refreshCallback={handleImageChange}
|
|
105
|
+
hasDefault={imageUrl}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
{showError && errorMessage && (
|
|
110
|
+
<Text
|
|
111
|
+
type="help"
|
|
112
|
+
style={{
|
|
113
|
+
color: "var(--colour-red)",
|
|
114
|
+
marginTop: "0.2rem",
|
|
115
|
+
display: "block",
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{errorMessage}
|
|
119
|
+
</Text>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Caption input field - only show if captions are enabled for this field type */}
|
|
124
|
+
{field.values.allowCaption && (
|
|
125
|
+
<div style={{ marginTop: "12px" }}>
|
|
126
|
+
<GenericInput
|
|
127
|
+
type="text"
|
|
128
|
+
value={caption}
|
|
129
|
+
placeholder="Add a caption for this image (optional)"
|
|
130
|
+
label="Image Caption"
|
|
131
|
+
onChange={(e) => handleCaptionChange(e.target.value)}
|
|
132
|
+
alwaysShowLabel
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{field.values.helpText && (
|
|
138
|
+
<Text
|
|
139
|
+
type="help"
|
|
140
|
+
style={{
|
|
141
|
+
color: "var(--text-bluegrey)",
|
|
142
|
+
marginTop: "8px",
|
|
143
|
+
display: "block",
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{field.values.helpText}
|
|
147
|
+
</Text>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default ListingImageInput;
|
|
@@ -0,0 +1,72 @@
|
|
|
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 ListingTextInput = ({
|
|
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.pen} />
|
|
42
|
+
</div>
|
|
43
|
+
<div className={styles.listingField__content}>
|
|
44
|
+
<h3 className={styles.listingField__title}>Title</h3>
|
|
45
|
+
<p className={styles.listingField__description}>
|
|
46
|
+
The main title for you listing
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<GenericInput
|
|
52
|
+
type="text"
|
|
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
|
+
/>
|
|
63
|
+
{helpText && (
|
|
64
|
+
<div style={{ marginTop: "8px", color: "#6c7a90", fontSize: "14px" }}>
|
|
65
|
+
{helpText}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default ListingTextInput;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// import * as PlussCore from '../../pluss-core/src';
|
|
2
|
+
import * as PlussCore from "@plusscommunities/pluss-core-web";
|
|
3
|
+
import { values } from "./values.config";
|
|
4
|
+
import { selectHasDefinition } from "./selectors/featureBuilderSelectors";
|
|
5
|
+
|
|
6
|
+
export { PlussCore };
|
|
7
|
+
|
|
8
|
+
const FeatureConfig = {
|
|
9
|
+
key: values.featureKey,
|
|
10
|
+
singularName: values.singularName,
|
|
11
|
+
description: values.description,
|
|
12
|
+
emptyText: values.emptyText,
|
|
13
|
+
widgetOptions: [],
|
|
14
|
+
hiddenFromFeaturePicker: (state) => {
|
|
15
|
+
// Hide feature builder from picker if no definitions exist
|
|
16
|
+
// This prevents users from adding feature builder when there are no feature definitions configured
|
|
17
|
+
return !selectHasDefinition(state);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
addUrl: values.routeFormOverviewStep,
|
|
21
|
+
baseUrl: values.featureKeyRoute,
|
|
22
|
+
addPermission: values.permissionFeatureBuilderDefinition,
|
|
23
|
+
activities: [],
|
|
24
|
+
permissions: [
|
|
25
|
+
{
|
|
26
|
+
displayName: values.textPermissionFeatureBuilderDefinition,
|
|
27
|
+
key: values.permissionFeatureBuilderDefinition,
|
|
28
|
+
hq: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
displayName: values.textPermissionFeatureBuilderContent,
|
|
32
|
+
key: values.permissionFeatureBuilderContent,
|
|
33
|
+
hq: false,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
menu: [
|
|
37
|
+
{
|
|
38
|
+
// HQ-only definition navitem
|
|
39
|
+
key: values.menuKey,
|
|
40
|
+
order: 16,
|
|
41
|
+
text: "Build Your Feature",
|
|
42
|
+
icon: values.defaultIcon,
|
|
43
|
+
isFontAwesome: true,
|
|
44
|
+
url: values.routeFormOverviewStep,
|
|
45
|
+
visibleExps: {
|
|
46
|
+
type: "and",
|
|
47
|
+
exps: [
|
|
48
|
+
{
|
|
49
|
+
type: "master",
|
|
50
|
+
value: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "hq",
|
|
54
|
+
value: true,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
order: values.menuOrder,
|
|
61
|
+
text: "",
|
|
62
|
+
icon: values.menuIcon,
|
|
63
|
+
isFontAwesome: values.menuIsFontAwesome,
|
|
64
|
+
url: values.featureKeyRoute + "/listing",
|
|
65
|
+
countProps: null,
|
|
66
|
+
visibleExps: {
|
|
67
|
+
type: "and",
|
|
68
|
+
exps: [
|
|
69
|
+
{ type: "feature", value: values.featureKey },
|
|
70
|
+
{ type: "permission", value: values.permissionFeatureBuilderContent },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
routes: [
|
|
76
|
+
{
|
|
77
|
+
path: values.routeFormOverviewStep,
|
|
78
|
+
component: values.screenFormOverviewStep,
|
|
79
|
+
exact: false, // Changed from exact:true to allow internal navigation
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: values.routeFormFieldsStep,
|
|
83
|
+
component: values.screenFormFieldsStep,
|
|
84
|
+
exact: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: values.routeFormLayoutStep,
|
|
88
|
+
component: values.screenFormLayoutStep,
|
|
89
|
+
exact: false,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
path: values.routeListingScreen,
|
|
93
|
+
component: values.screenListingScreen,
|
|
94
|
+
exact: true,
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
path: values.routeCreateListing,
|
|
99
|
+
component: values.pageCreateListing,
|
|
100
|
+
exact: false,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
path: values.routeEditListing,
|
|
104
|
+
component: values.pageEditListing,
|
|
105
|
+
exact: false,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
env: {
|
|
109
|
+
baseStage: "",
|
|
110
|
+
baseAPIUrl: "",
|
|
111
|
+
baseUploadsUrl: "",
|
|
112
|
+
uploadBucket: "",
|
|
113
|
+
colourBrandingMain: "",
|
|
114
|
+
colourBrandingOff: "",
|
|
115
|
+
colourBrandingApp: "",
|
|
116
|
+
defaultProfileImage: "",
|
|
117
|
+
utcOffset: "",
|
|
118
|
+
hasAvailableNews: false,
|
|
119
|
+
newsHaveTags: true,
|
|
120
|
+
defaultAllowComments: true,
|
|
121
|
+
makeApiKey: "",
|
|
122
|
+
logo: "",
|
|
123
|
+
clientName: "",
|
|
124
|
+
},
|
|
125
|
+
init: (environment) => {
|
|
126
|
+
FeatureConfig.env = environment;
|
|
127
|
+
PlussCore.Config.init(environment);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
export default FeatureConfig;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { PlussCore } from "../feature.config";
|
|
2
|
+
|
|
3
|
+
const { Helper } = PlussCore;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates if a string is a valid email format
|
|
7
|
+
*
|
|
8
|
+
* @param {string} email - Email address to validate
|
|
9
|
+
* @returns {boolean} True if valid email format, false otherwise
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Returns true
|
|
13
|
+
* isEmail('user@example.com');
|
|
14
|
+
* // Returns false
|
|
15
|
+
* isEmail('invalid-email');
|
|
16
|
+
*/
|
|
17
|
+
export const isEmail = Helper.isEmail;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the plural form of a word based on count
|
|
21
|
+
*
|
|
22
|
+
* @param {number} count - Number to determine plural form
|
|
23
|
+
* @returns {string} Plural form of the word
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Returns 'item'
|
|
27
|
+
* getPluralS(1);
|
|
28
|
+
* // Returns 'items'
|
|
29
|
+
* getPluralS(2);
|
|
30
|
+
*/
|
|
31
|
+
export const getPluralS = Helper.getPluralS;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parses URL parameters from a URL string
|
|
35
|
+
*
|
|
36
|
+
* @param {string} url - URL string to parse
|
|
37
|
+
* @returns {Object} Object containing URL parameters as key-value pairs
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Returns { id: '123', tab: 'profile' }
|
|
41
|
+
* getUrlParams('/users?id=123&tab=profile');
|
|
42
|
+
*/
|
|
43
|
+
export const getUrlParams = Helper.getUrlParams;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Safely reads and parses URL parameters
|
|
47
|
+
*
|
|
48
|
+
* @param {URLSearchParams} params - URL search parameters
|
|
49
|
+
* @returns {Object} Safely parsed parameters object
|
|
50
|
+
*/
|
|
51
|
+
export const safeReadParams = Helper.safeReadParams;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a 300x300 thumbnail URL
|
|
55
|
+
*
|
|
56
|
+
* @param {string} url - Original image URL
|
|
57
|
+
* @returns {string} Thumbnail URL with 300x300 dimensions
|
|
58
|
+
*/
|
|
59
|
+
export const getThumb300 = Helper.getThumb300;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets 1400x0 size dimensions
|
|
63
|
+
*
|
|
64
|
+
* @param {string} url - Image URL to analyze
|
|
65
|
+
* @returns {Object} Object containing width and height
|
|
66
|
+
*/
|
|
67
|
+
export const get1400 = Helper.get1400;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Converts time date pickers to UTC timestamps
|
|
71
|
+
*
|
|
72
|
+
* @param {Array} pickers - Array of time date picker objects
|
|
73
|
+
* @returns {Object} UTC timestamp conversion result
|
|
74
|
+
*/
|
|
75
|
+
export const getUTCFromTimeDatePickers = Helper.getUTCFromTimeDatePickers;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets pluralization options for different languages
|
|
79
|
+
*
|
|
80
|
+
* @returns {Object} Pluralization configuration object
|
|
81
|
+
*/
|
|
82
|
+
export const getPluralOptions = Helper.getPluralOptions;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Checks if a URL contains video content
|
|
86
|
+
*
|
|
87
|
+
* @param {string} url - URL to check
|
|
88
|
+
* @returns {boolean} True if contains video content
|
|
89
|
+
*/
|
|
90
|
+
export const isVideo = Helper.isVideo;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extracts filename from a URL or file path
|
|
94
|
+
*
|
|
95
|
+
* @param {string} path - Path or URL to extract filename from
|
|
96
|
+
* @returns {string} Filename without extension and path
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Returns 'document'
|
|
100
|
+
* getFileName('/path/to/document.pdf');
|
|
101
|
+
*/
|
|
102
|
+
export const getFileName = Helper.getFileName;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets site name from user roles array
|
|
106
|
+
*
|
|
107
|
+
* @param {Array} roles - Array of user role objects
|
|
108
|
+
* @returns {string} Site name derived from roles
|
|
109
|
+
*/
|
|
110
|
+
export const getSiteNameFromRoles = Helper.getSiteNameFromRoles;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Filters string to only alphanumeric characters
|
|
114
|
+
*
|
|
115
|
+
* @param {string} str - String to filter
|
|
116
|
+
* @returns {string} Filtered string containing only alphanumeric characters
|
|
117
|
+
*/
|
|
118
|
+
export const onlyAlphanumeric = Helper.onlyAlphanumeric;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Stores data in browser's localStorage
|
|
122
|
+
*
|
|
123
|
+
* @param {string} key - Storage key
|
|
124
|
+
* @param {*} value - Value to store
|
|
125
|
+
* @returns {boolean} True if successfully stored
|
|
126
|
+
*/
|
|
127
|
+
export const setLocalStorage = Helper.setLocalStorage;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reads and parses JSON data from localStorage
|
|
131
|
+
*
|
|
132
|
+
* @param {string} key - Storage key to read from
|
|
133
|
+
* @returns {Object|null} Parsed JSON object or null if not found
|
|
134
|
+
*/
|
|
135
|
+
export const readJSONFromStorage = Helper.readJSONFromStorage;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
3
|
+
import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex";
|
|
4
|
+
import {
|
|
5
|
+
selectDefinition,
|
|
6
|
+
selectDefinitionIsLoading,
|
|
7
|
+
selectDefinitionError,
|
|
8
|
+
} from "../selectors/featureBuilderSelectors";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom hook to handle loading feature definition in any step
|
|
12
|
+
* Ensures definition is loaded and provides loading/error states
|
|
13
|
+
* Triggers definition fetch when needed (e.g., on component mount or refresh)
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
* @param {boolean} [options.forceReload=false] - Force reload even if definition exists
|
|
17
|
+
* @returns {Object} Loading state and error information
|
|
18
|
+
* @returns {boolean} returns.isLoading - Whether definition is currently loading
|
|
19
|
+
* @returns {Error|null} returns.error - Any error that occurred during loading
|
|
20
|
+
* @returns {Object|null} returns.definition - The loaded definition (if available)
|
|
21
|
+
* @returns {Function} returns.reloadDefinition - Function to manually reload definition
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const { isLoading, error, definition, reloadDefinition } = useFeatureDefinitionLoader();
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Force reload on mount
|
|
28
|
+
* const { isLoading, error, definition } = useFeatureDefinitionLoader({ forceReload: true });
|
|
29
|
+
*/
|
|
30
|
+
export const useFeatureDefinitionLoader = (options = {}) => {
|
|
31
|
+
const { forceReload = false } = options;
|
|
32
|
+
const dispatch = useDispatch();
|
|
33
|
+
|
|
34
|
+
// Selectors for definition state
|
|
35
|
+
const definition = useSelector(selectDefinition);
|
|
36
|
+
const isLoading = useSelector(selectDefinitionIsLoading);
|
|
37
|
+
const error = useSelector(selectDefinitionError);
|
|
38
|
+
|
|
39
|
+
// Determine if we need to load the definition
|
|
40
|
+
const shouldLoadDefinition = !definition || forceReload;
|
|
41
|
+
|
|
42
|
+
// Function to manually reload definition
|
|
43
|
+
const reloadDefinition = () => {
|
|
44
|
+
dispatch(fetchFeatureDefinitions());
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Load definition when needed
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (shouldLoadDefinition && !isLoading) {
|
|
50
|
+
dispatch(fetchFeatureDefinitions());
|
|
51
|
+
}
|
|
52
|
+
}, [shouldLoadDefinition, isLoading, dispatch]);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
isLoading,
|
|
56
|
+
error,
|
|
57
|
+
definition,
|
|
58
|
+
reloadDefinition,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default useFeatureDefinitionLoader;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { FormOverviewStep } from "./screens/FormOverviewStep.jsx";
|
|
2
|
+
import { FormFieldsStep } from "./screens/FormFieldsStep.jsx";
|
|
3
|
+
import { FormLayoutStep } from "./screens/FormLayoutStep.jsx";
|
|
4
|
+
import { featureBuilderReducer } from "./reducers/featureBuilderReducer.js";
|
|
5
|
+
|
|
6
|
+
import ListingScreen from "./screens/ListingScreen.jsx";
|
|
7
|
+
import CreateListingPage from "./pages/CreateListingPage.jsx";
|
|
8
|
+
import EditListingPage from "./pages/EditListingPage.jsx";
|
|
9
|
+
import { values } from "./values.config.js";
|
|
10
|
+
import { fetchFeatureDefinitions } from "./actions/featureDefinitionsIndex";
|
|
11
|
+
|
|
12
|
+
export const Reducers = (() => {
|
|
13
|
+
const reducers = {};
|
|
14
|
+
reducers[values.reducerKey] = featureBuilderReducer;
|
|
15
|
+
return reducers;
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
export const Screens = (() => {
|
|
19
|
+
const screens = {};
|
|
20
|
+
|
|
21
|
+
screens["FormOverviewStep"] = FormOverviewStep;
|
|
22
|
+
screens["FormFieldsStep"] = FormFieldsStep;
|
|
23
|
+
screens["FormLayoutStep"] = FormLayoutStep;
|
|
24
|
+
screens["ListingScreen"] = ListingScreen;
|
|
25
|
+
screens["CreateListingPage"] = CreateListingPage;
|
|
26
|
+
screens["EditListingPage"] = EditListingPage;
|
|
27
|
+
return screens;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
export { default as Config } from "./feature.config.js";
|
|
31
|
+
export { default as ViewWidget } from "./components/ViewWidget.js"; // View on the feature selector - widget size
|
|
32
|
+
export { default as ViewFull } from "./components/ViewFull.js"; // View view on the feature selector - full size
|
|
33
|
+
export { default as PreviewWidget } from "./components/PreviewWidget.js"; // Preview on the phone - widget size
|
|
34
|
+
export { default as PreviewFull } from "./components/PreviewFull.js"; // Preview on the phone - full size
|
|
35
|
+
export { default as PreviewGrid } from "./components/PreviewGrid.js"; // Preview on the phone - grid mode
|
|
36
|
+
|
|
37
|
+
// Extension startup action for feature definition fetching
|
|
38
|
+
export const getStartupAction = () => fetchFeatureDefinitions();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useDispatch } from "react-redux";
|
|
3
|
+
import ListingEditor from "../components/ListingEditor.jsx";
|
|
4
|
+
import { ListingSuccessPopup } from "../components/ListingSuccessPopup.jsx";
|
|
5
|
+
import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex.js";
|
|
6
|
+
import { values } from "../values.config.js";
|
|
7
|
+
|
|
8
|
+
const CreateListingPage = ({ history }) => {
|
|
9
|
+
const dispatch = useDispatch();
|
|
10
|
+
const [showSuccessPopup, setShowSuccessPopup] = React.useState(false);
|
|
11
|
+
const [createdListingId, setCreatedListingId] = React.useState(null);
|
|
12
|
+
|
|
13
|
+
// Fetch feature definitions only for create mode
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
dispatch(fetchFeatureDefinitions());
|
|
16
|
+
}, [dispatch]);
|
|
17
|
+
|
|
18
|
+
const handleSuccess = (listingId) => {
|
|
19
|
+
setCreatedListingId(listingId);
|
|
20
|
+
setShowSuccessPopup(true);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleSuccessPopupClose = () => {
|
|
24
|
+
setShowSuccessPopup(false);
|
|
25
|
+
history.push(values.routeListingScreen);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleCancel = () => {
|
|
29
|
+
history.push(values.routeListingScreen);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<ListingEditor
|
|
35
|
+
mode="create"
|
|
36
|
+
onSuccess={handleSuccess}
|
|
37
|
+
onCancel={handleCancel}
|
|
38
|
+
/>
|
|
39
|
+
<ListingSuccessPopup
|
|
40
|
+
isOpen={showSuccessPopup}
|
|
41
|
+
onClose={handleSuccessPopupClose}
|
|
42
|
+
mode="create"
|
|
43
|
+
listingId={createdListingId}
|
|
44
|
+
/>
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default CreateListingPage;
|