@plusscommunities/pluss-feature-builder-web-a 1.0.2-beta.5 → 1.0.2-beta.7

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.
@@ -9,145 +9,145 @@ const { Apis } = PlussCore;
9
9
  const { fileActions } = Apis;
10
10
 
11
11
  const IconLoader = ({
12
- value,
13
- defaultValue,
14
- onChange,
15
- onRemove,
16
- disableRemove = false,
17
- featureId,
12
+ value,
13
+ defaultValue,
14
+ onChange,
15
+ onRemove,
16
+ disableRemove = false,
17
+ featureId,
18
18
  }) => {
19
- const [isUploading, setIsUploading] = useState(false);
20
- const fileInputRef = useRef(null);
21
-
22
- // Determine which icon to show
23
- const hasCustomIcon =
24
- value && typeof value === "string" && value.startsWith("http");
25
- const iconToShow = defaultValue || "star";
26
-
27
- // Generate filename with feature ID
28
- const generateFilename = (file, featureId) => {
29
- const timestamp = Date.now();
30
- const ext = file.name.split(".").pop();
31
- return `build-your-feature-${featureId || "new"}-${timestamp}.${ext}`;
32
- };
33
-
34
- // Get FontAwesome icon component directly (no async needed)
35
- const iconComponent = !hasCustomIcon ? iconImports[iconToShow] : null;
36
-
37
- const handleFileSelect = async (event) => {
38
- const file = event.target.files[0];
39
- if (!file || isUploading) return;
40
-
41
- // Validate file type - only accept images
42
- if (!file.type.startsWith("image/")) {
43
- alert("Please upload an image file (JPEG, PNG, GIF, or WebP)");
44
- return;
45
- }
46
-
47
- setIsUploading(true);
48
-
49
- try {
50
- // Compress and upload the image
51
- const compressedFile = await fileActions.compressImage(
52
- file,
53
- 1400,
54
- 0.8,
55
- false,
56
- );
57
-
58
- const filename = generateFilename(compressedFile, featureId);
59
- const url = await fileActions.uploadMediaAsync(compressedFile, filename);
60
-
61
- if (onChange) {
62
- onChange(url);
63
- }
64
- } catch (error) {
65
- alert("Failed to upload image. Please try again.");
66
- } finally {
67
- setIsUploading(false);
68
- // Reset file input
69
- if (fileInputRef.current) {
70
- fileInputRef.current.value = "";
71
- }
72
- }
73
- };
74
-
75
- const handleUploadClick = () => {
76
- if (fileInputRef.current && !isUploading) {
77
- fileInputRef.current.click();
78
- }
79
- };
80
-
81
- const handleRemove = () => {
82
- if (onRemove && !disableRemove) {
83
- onRemove();
84
- }
85
- };
86
-
87
- // Container will use CSS modules classes
88
- // Custom styling through props is deprecated in favor of CSS consistency
89
-
90
- return (
91
- <div className={`${styles.iconLoader} ${styles.iconLoader__container}`}>
92
- {/* Show the icon/image */}
93
- {hasCustomIcon ? (
94
- <img
95
- src={value}
96
- alt="Custom icon"
97
- className={styles.iconLoader__image}
98
- />
99
- ) : (
100
- <div className={styles.iconLoader__iconContainer}>
101
- {iconComponent ? (
102
- <FontAwesomeIcon
103
- icon={iconComponent}
104
- className={`${styles.iconLoader__icon} ${styles.iconLoader__iconLarge}`}
105
- />
106
- ) : (
107
- <div className={styles.iconLoader__fallbackText}>{iconToShow}</div>
108
- )}
109
- </div>
110
- )}
111
-
112
- {/* Upload/Delete button overlay - only shows on hover */}
113
- <div className={styles.iconLoader__buttonOverlay}>
114
- {hasCustomIcon ? (
115
- <Button
116
- buttonType="secondary"
117
- isActive
118
- onClick={handleRemove}
119
- disabled={disableRemove || isUploading}
120
- size="small"
121
- leftIcon="times"
122
- aria-label="Delete icon"
123
- >
124
- Delete
125
- </Button>
126
- ) : (
127
- <Button
128
- buttonType="primary"
129
- onClick={() => { }}
130
- disabled={isUploading}
131
- size="small"
132
- leftIcon="upload"
133
- loading={isUploading}
134
- aria-label="Upload icon"
135
- >
136
- Upload
137
- </Button>
138
- )}
139
- </div>
140
-
141
- {/* Hidden file input */}
142
- <input
143
- ref={fileInputRef}
144
- type="file"
145
- accept="image/jpeg,image/png,image/gif,image/webp,image/svg"
146
- onChange={handleFileSelect}
147
- className={styles.iconLoader__hiddenInput}
148
- />
149
- </div>
150
- );
19
+ const [isUploading, setIsUploading] = useState(false);
20
+ const fileInputRef = useRef(null);
21
+
22
+ // Determine which icon to show
23
+ const hasCustomIcon =
24
+ value && typeof value === "string" && value.startsWith("http");
25
+ const iconToShow = defaultValue || "star";
26
+
27
+ // Generate filename with feature ID
28
+ const generateFilename = (file, featureId) => {
29
+ const timestamp = Date.now();
30
+ const ext = file.name.split(".").pop();
31
+ return `build-your-feature-${featureId || "new"}-${timestamp}.${ext}`;
32
+ };
33
+
34
+ // Get FontAwesome icon component directly (no async needed)
35
+ const iconComponent = !hasCustomIcon ? iconImports[iconToShow] : null;
36
+
37
+ const handleFileSelect = async (event) => {
38
+ const file = event.target.files[0];
39
+ if (!file || isUploading) return;
40
+
41
+ // Validate file type - only accept images
42
+ if (!file.type.startsWith("image/")) {
43
+ alert("Please upload an image file (JPEG, PNG, GIF, or WebP)");
44
+ return;
45
+ }
46
+
47
+ setIsUploading(true);
48
+
49
+ try {
50
+ // Compress and upload the image
51
+ const compressedFile = await fileActions.compressImage(
52
+ file,
53
+ 1400,
54
+ 0.8,
55
+ false,
56
+ );
57
+
58
+ const filename = generateFilename(compressedFile, featureId);
59
+ const url = await fileActions.uploadMediaAsync(compressedFile, filename);
60
+
61
+ if (onChange) {
62
+ onChange(url);
63
+ }
64
+ } catch (error) {
65
+ alert("Failed to upload image. Please try again.");
66
+ } finally {
67
+ setIsUploading(false);
68
+ // Reset file input
69
+ if (fileInputRef.current) {
70
+ fileInputRef.current.value = "";
71
+ }
72
+ }
73
+ };
74
+
75
+ const handleUploadClick = () => {
76
+ if (fileInputRef.current && !isUploading) {
77
+ fileInputRef.current.click();
78
+ }
79
+ };
80
+
81
+ const handleRemove = () => {
82
+ if (onRemove && !disableRemove) {
83
+ onRemove();
84
+ }
85
+ };
86
+
87
+ // Container will use CSS modules classes
88
+ // Custom styling through props is deprecated in favor of CSS consistency
89
+
90
+ return (
91
+ <div className={`${styles.iconLoader} ${styles.iconLoader__container}`}>
92
+ {/* Show the icon/image */}
93
+ {hasCustomIcon ? (
94
+ <img
95
+ src={value}
96
+ alt="Custom icon"
97
+ className={styles.iconLoader__image}
98
+ />
99
+ ) : (
100
+ <div className={styles.iconLoader__iconContainer}>
101
+ {iconComponent ? (
102
+ <FontAwesomeIcon
103
+ icon={iconComponent}
104
+ className={`${styles.iconLoader__icon} ${styles.iconLoader__iconLarge}`}
105
+ />
106
+ ) : (
107
+ <div className={styles.iconLoader__fallbackText}>{iconToShow}</div>
108
+ )}
109
+ </div>
110
+ )}
111
+
112
+ {/* Upload/Delete button overlay - only shows on hover */}
113
+ <div className={styles.iconLoader__buttonOverlay}>
114
+ {hasCustomIcon ? (
115
+ <Button
116
+ buttonType="secondary"
117
+ isActive
118
+ onClick={handleRemove}
119
+ disabled={disableRemove || isUploading}
120
+ size="small"
121
+ leftIcon="times"
122
+ aria-label="Delete icon"
123
+ >
124
+ Delete
125
+ </Button>
126
+ ) : (
127
+ <Button
128
+ buttonType="primary"
129
+ onClick={() => {}}
130
+ disabled={isUploading}
131
+ size="small"
132
+ leftIcon="upload"
133
+ loading={isUploading}
134
+ aria-label="Upload icon"
135
+ >
136
+ Upload
137
+ </Button>
138
+ )}
139
+ </div>
140
+
141
+ {/* Hidden file input */}
142
+ <input
143
+ ref={fileInputRef}
144
+ type="file"
145
+ accept="image/jpeg,image/png,image/gif,image/webp,image/svg"
146
+ onChange={handleFileSelect}
147
+ className={styles.iconLoader__hiddenInput}
148
+ />
149
+ </div>
150
+ );
151
151
  };
152
152
 
153
153
  export default IconLoader;
@@ -6,7 +6,6 @@ import { allIcons, defaultIcon } from "./iconCategories";
6
6
  import { iconImports } from "./iconImports";
7
7
  import styles from "./IconSelector.module.css";
8
8
 
9
-
10
9
  const IconSelector = ({
11
10
  selectedIcon,
12
11
  onIconSelect,
@@ -32,7 +32,6 @@ import {
32
32
  import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex.js";
33
33
  import { values } from "../values.config.js";
34
34
 
35
-
36
35
  const ListingEditor = ({ mode = "create", listingId, onSuccess, onCancel }) => {
37
36
  const isEditMode = mode === "edit";
38
37
  const dispatch = useDispatch();
@@ -149,7 +149,10 @@ const SideBarInner = (props) => {
149
149
  stepsWithUrls.forEach((step, index) => {
150
150
  const navItem = Array.from(
151
151
  document.querySelectorAll(".hub-wrapperContainer .sideNav-item"),
152
- ).find((item) => item.textContent && item.textContent.includes(`${index + 1}.`));
152
+ ).find(
153
+ (item) =>
154
+ item.textContent && item.textContent.includes(`${index + 1}.`),
155
+ );
153
156
 
154
157
  if (navItem) {
155
158
  // Remove any existing click listeners
@@ -57,5 +57,3 @@ export { default as ToastContainer } from "./ToastContainer.jsx";
57
57
  // Gallery components
58
58
  export { default as ListingGalleryInput } from "./listing/ListingGalleryInput.jsx";
59
59
  export { default as GalleryDisplay } from "./listing/GalleryDisplay.jsx";
60
-
61
-
@@ -4,7 +4,6 @@ import { Text } from "../../components";
4
4
  import { iconImports } from "../../components/iconImports.js";
5
5
  import styles from "./GalleryDisplay.module.css";
6
6
 
7
-
8
7
  export const GalleryDisplay = (props) => {
9
8
  const {
10
9
  images = [],
@@ -186,7 +186,10 @@ const ListingGalleryInput = ({
186
186
  >
187
187
  {images.map((image, index) => {
188
188
  return (
189
- <div key={`image-${index}`} className={galleryStyles.imageItem}>
189
+ <div
190
+ key={`image-${index}`}
191
+ className={galleryStyles.imageItem}
192
+ >
190
193
  <img
191
194
  src={image}
192
195
  alt={`Gallery item ${index + 1}`}
package/src/index.js CHANGED
@@ -18,12 +18,12 @@ export const Reducers = (() => {
18
18
  export const Screens = (() => {
19
19
  const screens = {};
20
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;
21
+ screens[values.screenFormOverviewStep] = FormOverviewStep;
22
+ screens[values.screenFormFieldsStep] = FormFieldsStep;
23
+ screens[values.screenFormLayoutStep] = FormLayoutStep;
24
+ screens[values.screenListingScreen] = ListingScreen;
25
+ screens[values.pageCreateListing] = CreateListingPage;
26
+ screens[values.pageEditListing] = EditListingPage;
27
27
  return screens;
28
28
  })();
29
29
 
@@ -277,28 +277,21 @@ const formReducer = (state = INITIAL_FORM_STATE, action) => {
277
277
  switch (type) {
278
278
  case formActionTypes.SET_INITIAL_VALUES: {
279
279
  // The actual definition data is in payload.featureDefinition.definition
280
- const definitionWrapper =
281
- payload && payload.featureDefinition
282
- ? payload.featureDefinition
283
- : payload;
284
- const definition =
285
- definitionWrapper && definitionWrapper.definition
286
- ? definitionWrapper.definition
287
- : definitionWrapper;
280
+ const definitionWrapper = (payload && payload.featureDefinition) || payload;
281
+ const definition = (definitionWrapper && definitionWrapper.definition) || definitionWrapper;
288
282
 
289
283
  // Validate and map definition data to form state structure
290
284
  const mappedValues = {
291
- title: (definition && definition.title) || "",
292
- icon: (definition && definition.icon) || "star",
293
- displayName: (definition && definition.displayName) || "",
294
- layout: (definition && definition.layout) || {
285
+ title: definition?.title || "",
286
+ icon: definition?.icon || "star",
287
+ displayName: definition?.displayName || "",
288
+ layout: definition?.layout || {
295
289
  gridIcon: undefined,
296
290
  type: "round",
297
291
  },
298
- fields:
299
- definition && Array.isArray(definition.fields)
300
- ? definition.fields
301
- : state.fields,
292
+ fields: Array.isArray(definition?.fields)
293
+ ? definition.fields
294
+ : state.fields,
302
295
  };
303
296
 
304
297
  const newState = {
@@ -518,17 +511,13 @@ const definitionReducer = (state = INITIAL_STATE.definition, action) => {
518
511
  // Extract from API response for edit mode
519
512
  // Handle nested structure: data.featureDefinition.definition
520
513
  const featureDefinitionWrapper =
521
- data && data.featureDefinition ? data.featureDefinition : data;
514
+ (data && data.featureDefinition) || data;
522
515
  definition =
523
- featureDefinitionWrapper && featureDefinitionWrapper.definition
524
- ? featureDefinitionWrapper.definition
525
- : featureDefinitionWrapper;
526
- definitionId =
527
- (featureDefinitionWrapper && featureDefinitionWrapper.id) ||
528
- values.featureId;
516
+ (featureDefinitionWrapper && featureDefinitionWrapper.definition) || featureDefinitionWrapper;
517
+ definitionId = (featureDefinitionWrapper && featureDefinitionWrapper.id) || values.featureId;
529
518
 
530
519
  // Ensure fields array exists and preserves order property
531
- if (definition && definition.fields) {
520
+ if (definition?.fields) {
532
521
  // Create a new array to ensure we preserve all field properties including order
533
522
  definition.fields = definition.fields.map((field) => ({
534
523
  ...field,
@@ -566,7 +555,7 @@ const definitionReducer = (state = INITIAL_STATE.definition, action) => {
566
555
  definition: payload, // Optimistically update with new definition
567
556
  mode: "edit", // Switch to edit mode immediately
568
557
  },
569
- id: payload && payload.id,
558
+ id: payload?.id,
570
559
  error: null,
571
560
  };
572
561
 
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect } from "react";
2
2
  import { SidebarLayout } from "../components/SidebarLayout.jsx";
3
3
  import { values } from "../values.config.js";
4
4
  import { PlussCore } from "../feature.config";
@@ -7,10 +7,8 @@ import {
7
7
  Text,
8
8
  Button,
9
9
  LoadingState,
10
- SkeletonLoader,
11
10
  IconLoader,
12
11
  ErrorBoundary,
13
- CenteredContainer,
14
12
  FeatureBuilderSuccessPopup,
15
13
  } from "../components";
16
14
  import ToastContainer from "../components/ToastContainer.jsx";
@@ -20,7 +18,6 @@ import {
20
18
  setGridLayoutIcon,
21
19
  submitForm,
22
20
  clearFormSubmissionState,
23
- setDisplayName,
24
21
  setInitialValues,
25
22
  } from "../actions/formActions";
26
23
  import {
@@ -30,7 +27,6 @@ import {
30
27
  selectIsEditMode,
31
28
  selectIsStepValid,
32
29
  selectStepErrors,
33
- selectCurrentStep,
34
30
  selectFormIsSubmitting,
35
31
  selectFormSubmitError,
36
32
  selectFormSubmitSuccess,
@@ -61,8 +57,7 @@ const FormLayoutStepInner = (props) => {
61
57
  const isEditMode = useSelector(selectIsEditMode);
62
58
 
63
59
  // Use custom hook to handle definition loading
64
- const { definition, definitionIsLoading, reloadDefinition } =
65
- useFeatureDefinitionLoader();
60
+ const { definition, definitionIsLoading } = useFeatureDefinitionLoader();
66
61
 
67
62
  // Get form initialization state
68
63
  const isFormInitial = useSelector(selectFormIsInitial);
@@ -145,25 +140,29 @@ const FormLayoutStepInner = (props) => {
145
140
  value: "round",
146
141
  title: "Round Images",
147
142
  description: "Round photos in a grid",
148
- image: "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/b8156f584c92a0edbe13a8e05d/fblayoutround.png",
143
+ image:
144
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/b8156f584c92a0edbe13a8e05d/fblayoutround.png",
149
145
  },
150
146
  {
151
147
  value: "condensed",
152
148
  title: "Compact List",
153
149
  description: "Small photos in a list",
154
- image: "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/dfec30d342249a4073e5ffc6b8/fblayoutcompact.png",
150
+ image:
151
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/dfec30d342249a4073e5ffc6b8/fblayoutcompact.png",
155
152
  },
156
153
  {
157
154
  value: "square",
158
155
  title: "Square Images",
159
156
  description: "Square photos in a grid",
160
- image: "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/771e4626462a93041746a746c8/fblayoutsquare.png",
157
+ image:
158
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/771e4626462a93041746a746c8/fblayoutsquare.png",
161
159
  },
162
160
  {
163
161
  value: "feature",
164
162
  title: "Large Photos",
165
163
  description: "Big photos with details",
166
- image: "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/f48acc614508ba246186b12845/fblayoutcardslarge.png",
164
+ image:
165
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/f48acc614508ba246186b12845/fblayoutcardslarge.png",
167
166
  },
168
167
  ];
169
168
 
@@ -314,10 +313,8 @@ const FormLayoutStepInner = (props) => {
314
313
  onChange={() => { }} // Disabled
315
314
  onRemove={() => { }} // Disabled
316
315
  featureId={definitionId || "new"}
317
-
318
316
  />
319
317
 
320
-
321
318
  <Text type="help" color="#6c757d" className="marginTop-16">
322
319
  We're working on bringing the ability to use custom grid icons
323
320
  </Text>