@openlettermarketing/olc-react-sdk 2.1.4 → 2.1.5-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.eslintrc.yml +47 -0
  4. package/.github/workflows/publish-beta.yml +154 -0
  5. package/.github/workflows/publish-production.yml +143 -0
  6. package/.prettierignore +3 -0
  7. package/.prettierrc.yml +5 -0
  8. package/CHANGELOG.md +4 -0
  9. package/babel.config.json +10 -0
  10. package/build/index.js +82 -82
  11. package/build/index.js.map +1 -1
  12. package/build/types/version.d.ts +1 -1
  13. package/examples/.eslintrc.yml +4 -0
  14. package/index.html +18 -0
  15. package/package.json +1 -1
  16. package/public/vite.svg +1 -0
  17. package/src/App.tsx +209 -0
  18. package/src/assets/Fonts/Lexi-Regular.ttf +0 -0
  19. package/src/assets/images/create-template/prebuilt.svg +13 -0
  20. package/src/assets/images/create-template/scratch.svg +4 -0
  21. package/src/assets/images/input/cancel.tsx +20 -0
  22. package/src/assets/images/input/search.tsx +20 -0
  23. package/src/assets/images/input/select-cancel.tsx +17 -0
  24. package/src/assets/images/modal-icons/add.tsx +36 -0
  25. package/src/assets/images/modal-icons/cancel-file.tsx +12 -0
  26. package/src/assets/images/modal-icons/cancel-input.tsx +13 -0
  27. package/src/assets/images/modal-icons/cancel.tsx +35 -0
  28. package/src/assets/images/modal-icons/close-new.svg +3 -0
  29. package/src/assets/images/modal-icons/confirm-close-icon.tsx +14 -0
  30. package/src/assets/images/modal-icons/confirm-new.tsx +22 -0
  31. package/src/assets/images/modal-icons/confirm.svg +12 -0
  32. package/src/assets/images/modal-icons/cross.tsx +23 -0
  33. package/src/assets/images/modal-icons/del.tsx +19 -0
  34. package/src/assets/images/modal-icons/design-icon.tsx +22 -0
  35. package/src/assets/images/modal-icons/doc.tsx +43 -0
  36. package/src/assets/images/modal-icons/docx.tsx +43 -0
  37. package/src/assets/images/modal-icons/envelope-icon.tsx +26 -0
  38. package/src/assets/images/modal-icons/info.tsx +19 -0
  39. package/src/assets/images/modal-icons/jpeg.tsx +43 -0
  40. package/src/assets/images/modal-icons/jpg.tsx +43 -0
  41. package/src/assets/images/modal-icons/modal-cros.svg +4 -0
  42. package/src/assets/images/modal-icons/modal-cross.tsx +37 -0
  43. package/src/assets/images/modal-icons/new-cancel.tsx +11 -0
  44. package/src/assets/images/modal-icons/order-download.tsx +42 -0
  45. package/src/assets/images/modal-icons/pdf.tsx +51 -0
  46. package/src/assets/images/modal-icons/png.tsx +43 -0
  47. package/src/assets/images/modal-icons/save.tsx +23 -0
  48. package/src/assets/images/modal-icons/template-copy.tsx +25 -0
  49. package/src/assets/images/modal-icons/tool-cancel.tsx +25 -0
  50. package/src/assets/images/products/bi-new.svg +23 -0
  51. package/src/assets/images/products/left-arrow.svg +17 -0
  52. package/src/assets/images/products/personal-new.tsx +31 -0
  53. package/src/assets/images/products/postcard-new.tsx +27 -0
  54. package/src/assets/images/products/professional-new.tsx +24 -0
  55. package/src/assets/images/products/real-new.tsx +30 -0
  56. package/src/assets/images/products/right-arrow.svg +17 -0
  57. package/src/assets/images/products/snap-new.svg +31 -0
  58. package/src/assets/images/templates/actions.svg +3 -0
  59. package/src/assets/images/templates/address-block-icon.tsx +62 -0
  60. package/src/assets/images/templates/archive.svg +3 -0
  61. package/src/assets/images/templates/arrow-down.tsx +27 -0
  62. package/src/assets/images/templates/back-arrow.tsx +19 -0
  63. package/src/assets/images/templates/bi-fold-self-mailers.tsx +28 -0
  64. package/src/assets/images/templates/check.svg +3 -0
  65. package/src/assets/images/templates/code.svg +10 -0
  66. package/src/assets/images/templates/content-copy-icon.tsx +24 -0
  67. package/src/assets/images/templates/custom-add-on-icon.tsx +18 -0
  68. package/src/assets/images/templates/custom-qr-section-icon.tsx +9 -0
  69. package/src/assets/images/templates/custom-template.tsx +23 -0
  70. package/src/assets/images/templates/designer.tsx +43 -0
  71. package/src/assets/images/templates/dot.tsx +22 -0
  72. package/src/assets/images/templates/download-v2.svg +4 -0
  73. package/src/assets/images/templates/download.svg +4 -0
  74. package/src/assets/images/templates/dummy-template.tsx +76 -0
  75. package/src/assets/images/templates/dynamic-field.tsx +119 -0
  76. package/src/assets/images/templates/edit-pencil-icon.tsx +21 -0
  77. package/src/assets/images/templates/edit.svg +3 -0
  78. package/src/assets/images/templates/epo-icon.tsx +16 -0
  79. package/src/assets/images/templates/field.tsx +29 -0
  80. package/src/assets/images/templates/gsv-icon.tsx +31 -0
  81. package/src/assets/images/templates/info-icon.tsx +37 -0
  82. package/src/assets/images/templates/left-arrow.svg +17 -0
  83. package/src/assets/images/templates/pencil.svg +3 -0
  84. package/src/assets/images/templates/personal-letter.tsx +53 -0
  85. package/src/assets/images/templates/postcard.tsx +32 -0
  86. package/src/assets/images/templates/professional-letter.tsx +53 -0
  87. package/src/assets/images/templates/qr-code.tsx +13 -0
  88. package/src/assets/images/templates/real-penned-letters.tsx +57 -0
  89. package/src/assets/images/templates/right-arrow.svg +17 -0
  90. package/src/assets/images/templates/size-image-lg.tsx +20 -0
  91. package/src/assets/images/templates/size-image-mid.tsx +20 -0
  92. package/src/assets/images/templates/size-image-xl.tsx +20 -0
  93. package/src/assets/images/templates/size-image.tsx +20 -0
  94. package/src/assets/images/templates/snap-pack.tsx +67 -0
  95. package/src/assets/images/templates/template-default-design.tsx +21 -0
  96. package/src/assets/images/templates/trash-upload.svg +3 -0
  97. package/src/assets/images/templates/trash.svg +3 -0
  98. package/src/assets/images/templates/tri-fold-self-mailers.tsx +93 -0
  99. package/src/assets/images/templates/upload-image.svg +10 -0
  100. package/src/assets/images/templates/x.svg +3 -0
  101. package/src/assets/images/thumbnails/one.svg +9 -0
  102. package/src/assets/images/tooltip/tool-arrow.tsx +25 -0
  103. package/src/components/CreateTemplate/V2/index.tsx +525 -0
  104. package/src/components/CreateTemplate/V2/styles.scss +372 -0
  105. package/src/components/CreateTemplate/index.tsx +508 -0
  106. package/src/components/CreateTemplate/styles.scss +404 -0
  107. package/src/components/GenericUIBlocks/Button/index.tsx +54 -0
  108. package/src/components/GenericUIBlocks/Button/styles.scss +43 -0
  109. package/src/components/GenericUIBlocks/CircularProgress/index.tsx +18 -0
  110. package/src/components/GenericUIBlocks/CircularProgress/styles.scss +93 -0
  111. package/src/components/GenericUIBlocks/CustomTooltip/index.tsx +88 -0
  112. package/src/components/GenericUIBlocks/CustomTooltip/styles.scss +19 -0
  113. package/src/components/GenericUIBlocks/Dialog/V2/index.tsx +227 -0
  114. package/src/components/GenericUIBlocks/Dialog/V2/styles.scss +289 -0
  115. package/src/components/GenericUIBlocks/Dialog/index.tsx +185 -0
  116. package/src/components/GenericUIBlocks/Dialog/styles.scss +227 -0
  117. package/src/components/GenericUIBlocks/Divider/index.tsx +12 -0
  118. package/src/components/GenericUIBlocks/Divider/styles.scss +7 -0
  119. package/src/components/GenericUIBlocks/GeneralSelect/index.tsx +114 -0
  120. package/src/components/GenericUIBlocks/GeneralSelect/styles.scss +406 -0
  121. package/src/components/GenericUIBlocks/GeneralTooltip/index.tsx +25 -0
  122. package/src/components/GenericUIBlocks/GeneralTooltip/styles.scss +20 -0
  123. package/src/components/GenericUIBlocks/GenericSnackbar/Toast/index.tsx +91 -0
  124. package/src/components/GenericUIBlocks/GenericSnackbar/Toast/styles.scss +92 -0
  125. package/src/components/GenericUIBlocks/Grid/index.tsx +82 -0
  126. package/src/components/GenericUIBlocks/Input/index.tsx +269 -0
  127. package/src/components/GenericUIBlocks/Input/styles.scss +332 -0
  128. package/src/components/GenericUIBlocks/Tabs/index.tsx +71 -0
  129. package/src/components/GenericUIBlocks/Tabs/styles.scss +42 -0
  130. package/src/components/GenericUIBlocks/Typography/index.tsx +18 -0
  131. package/src/components/GenericUIBlocks/Typography/styles.scss +27 -0
  132. package/src/components/SidePanel/CustomAddOns/index.tsx +342 -0
  133. package/src/components/SidePanel/CustomAddOns/styles.scss +86 -0
  134. package/src/components/SidePanel/CustomBlockColors/index.tsx +211 -0
  135. package/src/components/SidePanel/CustomBlockColors/styles.scss +80 -0
  136. package/src/components/SidePanel/CustomFields/customFieldSection.tsx +547 -0
  137. package/src/components/SidePanel/CustomFields/styles.scss +64 -0
  138. package/src/components/SidePanel/CustomQRCode/V2/QRCodeModal/index.tsx +172 -0
  139. package/src/components/SidePanel/CustomQRCode/V2/QRCodeModal/styles.scss +46 -0
  140. package/src/components/SidePanel/CustomQRCode/index.tsx +1070 -0
  141. package/src/components/SidePanel/CustomQRCode/styles.scss +149 -0
  142. package/src/components/SidePanel/CustomUploads/V2/index.tsx +542 -0
  143. package/src/components/SidePanel/CustomUploads/V2/styles.scss +267 -0
  144. package/src/components/SidePanel/CustomUploads/index.tsx +301 -0
  145. package/src/components/SidePanel/Templates/ModalGallery/HireDesigner/index.tsx +424 -0
  146. package/src/components/SidePanel/Templates/ModalGallery/HireDesigner/styles.scss +180 -0
  147. package/src/components/SidePanel/Templates/ModalGallery/V2/index.tsx +235 -0
  148. package/src/components/SidePanel/Templates/ModalGallery/V2/styles.scss +244 -0
  149. package/src/components/SidePanel/Templates/ModalGallery/index.tsx +231 -0
  150. package/src/components/SidePanel/Templates/SideBarGallery/index.tsx +233 -0
  151. package/src/components/SidePanel/Templates/SideBarGallery/styles.scss +152 -0
  152. package/src/components/SidePanel/Templates/TemplatesCard/V2/index.tsx +149 -0
  153. package/src/components/SidePanel/Templates/TemplatesCard/V2/styles.scss +156 -0
  154. package/src/components/SidePanel/Templates/TemplatesCard/index.tsx +160 -0
  155. package/src/components/SidePanel/Templates/TemplatesCard/styles.scss +98 -0
  156. package/src/components/SidePanel/Templates/customTemplateSection.tsx +793 -0
  157. package/src/components/SidePanel/Templates/styles.scss +244 -0
  158. package/src/components/SidePanel/index.tsx +160 -0
  159. package/src/components/TemplateBuilder/index.tsx +585 -0
  160. package/src/components/TemplateBuilder/styles.scss +100 -0
  161. package/src/components/TemplateTypes/index.tsx +96 -0
  162. package/src/components/TemplateTypes/styles.scss +91 -0
  163. package/src/components/TopNavigation/ConfirmNavigateDialog/index.tsx +81 -0
  164. package/src/components/TopNavigation/ConfirmNavigateDialog/styles.scss +123 -0
  165. package/src/components/TopNavigation/DuplicateTemplateModal.tsx +103 -0
  166. package/src/components/TopNavigation/EditTemplateNameModel/index.tsx +71 -0
  167. package/src/components/TopNavigation/EditTemplateNameModel/styles.scss +88 -0
  168. package/src/components/TopNavigation/SaveTemplateModel/index.tsx +201 -0
  169. package/src/components/TopNavigation/SaveTemplateModel/styles.scss +128 -0
  170. package/src/components/TopNavigation/index.tsx +938 -0
  171. package/src/components/TopNavigation/styles.scss +303 -0
  172. package/src/importMeta.d.ts +31 -0
  173. package/src/index.scss +131 -0
  174. package/src/index.tsx +238 -0
  175. package/src/libs/test.ts +7 -0
  176. package/src/redux/actions/action-types.ts +52 -0
  177. package/src/redux/actions/customQRCodeActions.ts +54 -0
  178. package/src/redux/actions/snackbarActions.ts +16 -0
  179. package/src/redux/actions/templateActions.ts +236 -0
  180. package/src/redux/reducers/customFieldReducer.ts +99 -0
  181. package/src/redux/reducers/customQRCodeReducer.ts +58 -0
  182. package/src/redux/reducers/index.ts +15 -0
  183. package/src/redux/reducers/snackbarReducer.ts +40 -0
  184. package/src/redux/reducers/templateReducer.ts +485 -0
  185. package/src/redux/store.ts +18 -0
  186. package/src/styles/colors.scss +61 -0
  187. package/src/test/mocks.js +89 -0
  188. package/src/test/setupJest.js +1 -0
  189. package/src/utils/api.ts +36 -0
  190. package/src/utils/constants.ts +182 -0
  191. package/src/utils/customStyles.ts +45 -0
  192. package/src/utils/fetchWrapper.ts +73 -0
  193. package/src/utils/fonts.json +1597 -0
  194. package/src/utils/helper.ts +205 -0
  195. package/src/utils/local-storage.ts +15 -0
  196. package/src/utils/message.ts +162 -0
  197. package/src/utils/products.ts +186 -0
  198. package/src/utils/template-builder.ts +328 -0
  199. package/src/utils/templateIdentifierArea/biFold.ts +107 -0
  200. package/src/utils/templateIdentifierArea/index.ts +35 -0
  201. package/src/utils/templateIdentifierArea/personal.ts +107 -0
  202. package/src/utils/templateIdentifierArea/postCards.ts +163 -0
  203. package/src/utils/templateIdentifierArea/professional.ts +125 -0
  204. package/src/utils/templateIdentifierArea/snapPack.ts +107 -0
  205. package/src/utils/templateIdentifierArea/triFold.ts +107 -0
  206. package/src/utils/templateRestrictedArea/biFold.ts +329 -0
  207. package/src/utils/templateRestrictedArea/nonWindowProfessional.ts +90 -0
  208. package/src/utils/templateRestrictedArea/personal.ts +90 -0
  209. package/src/utils/templateRestrictedArea/postCard.ts +334 -0
  210. package/src/utils/templateRestrictedArea/postCardJumbo.tsx +408 -0
  211. package/src/utils/templateRestrictedArea/professional.ts +318 -0
  212. package/src/utils/templateRestrictedArea/realPenned.ts +233 -0
  213. package/src/utils/templateRestrictedArea/snapPack.ts +1009 -0
  214. package/src/utils/templateRestrictedArea/triFold.ts +330 -0
  215. package/src/utils/templateSafetyBorders/biFold.ts +91 -0
  216. package/src/utils/templateSafetyBorders/index.ts +43 -0
  217. package/src/utils/templateSafetyBorders/personal.ts +41 -0
  218. package/src/utils/templateSafetyBorders/postCards.ts +259 -0
  219. package/src/utils/templateSafetyBorders/professional.ts +78 -0
  220. package/src/utils/templateSafetyBorders/snapPack.ts +165 -0
  221. package/src/utils/templateSafetyBorders/triFold.ts +114 -0
  222. package/src/utils/templateSafetyBorders/types.d.ts +68 -0
  223. package/src/utils/types.ts +12 -0
  224. package/src/v2Theme.scss +142 -0
  225. package/tsconfig.json +29 -0
  226. package/tsconfig.node.json +12 -0
  227. package/update-version.js +23 -0
  228. package/version.js +1 -0
  229. package/vite.config.ts +8 -0
  230. package/webpack.config.js +80 -0
@@ -0,0 +1,149 @@
1
+ .qr-card{
2
+ display: flex;
3
+ justify-content: flex-start;
4
+ align-items: center;
5
+ gap: 8px;
6
+ border: 0.5px solid #0000003D;
7
+ padding: 8px;
8
+ border-radius: 4px;
9
+ position: relative;
10
+ cursor: pointer;
11
+
12
+ .qr-card-content{
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 4px;
16
+ }
17
+
18
+ .actions-container{
19
+ position: absolute;
20
+ right: 12px;
21
+ top: 10px;
22
+ }
23
+
24
+ .actions{
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ cursor: pointer;
29
+ padding: 7px;
30
+ transition: all 0.3s ease;
31
+
32
+ &:hover{
33
+ background-color: #f5f5f5;
34
+ border-radius: 4px;
35
+ }
36
+ }
37
+
38
+ .actions-wrapper{
39
+ position: absolute;
40
+ top: calc(100% + 4px);
41
+ right: -8px;
42
+ background-color: #FFFFFF;
43
+ padding: 12px 8px;
44
+ box-shadow: 2px 8px 24px 0px #00000029;
45
+ gap: 6px;
46
+ display: flex;
47
+ flex-direction: column;
48
+ border-radius: 4px;
49
+ z-index: 10;
50
+ min-width: 100px;
51
+
52
+ &.actions-wrapper-up {
53
+ top: auto;
54
+ bottom: calc(100% + 4px);
55
+ }
56
+
57
+ .action-item{
58
+ display: flex;
59
+ justify-content: flex-start;
60
+ align-items: center;
61
+ gap: 8px;
62
+ cursor: pointer;
63
+ padding: 4px 8px;
64
+ border-radius: 4px;
65
+ transition: background-color 0.2s ease;
66
+
67
+ &:hover{
68
+ background-color: #f5f5f5;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // V2 Styles for API-driven functionality
75
+ .qr-section-v2-container {
76
+ display: flex;
77
+ flex-direction: column;
78
+ height: 100%;
79
+ min-height: 0;
80
+
81
+ .qr-code-wrapper-sticky {
82
+ flex-shrink: 0;
83
+ }
84
+ }
85
+
86
+ .qr-codes-list-scroll {
87
+ flex: 1;
88
+ min-height: 0;
89
+ overflow-y: auto;
90
+ margin-top: 16px;
91
+ }
92
+
93
+ .qr-codes-grid {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 8px;
97
+
98
+ .loading-state, .empty-state {
99
+ text-align: center;
100
+ padding: 20px;
101
+ color: #666;
102
+ font-size: 14px;
103
+ }
104
+
105
+ .loading-more {
106
+ padding: 10px;
107
+ text-align: center;
108
+ }
109
+ }
110
+
111
+ .qr-input-wrapper{
112
+ label{
113
+ font-weight: 600;
114
+ line-height: 19px;
115
+ }
116
+ &:not(:first-child){
117
+ margin-top: 15px;
118
+ }
119
+ }
120
+
121
+ .qr-input-wrapper-v2{
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 8px;
125
+
126
+ label{
127
+ font-weight: 500;
128
+ line-height: 19px;
129
+ }
130
+ }
131
+ .qr-submit-btn{
132
+ width: 100%;
133
+ padding: 10px;
134
+ background-color: #5e5e5e;
135
+ box-shadow: inset 0 0 0 1px #11141833,0 1px 2px #1114181a;
136
+ color: #fff;
137
+ border: none;
138
+ cursor: pointer;
139
+ font-weight: 600;
140
+ border-radius: 3px;
141
+ transition: all 0.3s ease;
142
+ &:focus{
143
+ outline: none;
144
+ }
145
+ &:hover{
146
+ background-color: #383838;
147
+ }
148
+ }
149
+
@@ -0,0 +1,542 @@
1
+ import React, { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import type { StoreType } from 'polotno/model/store';
4
+ import { getCrop } from 'polotno/utils/image';
5
+
6
+ // redux
7
+ import { useDispatch } from 'react-redux';
8
+ import { AppDispatch } from '../../../../redux/store';
9
+
10
+ // Utils
11
+ import { getPublicApiKey, getType } from '../../../../utils/helper';
12
+ import { allowedImageTypes } from '../../../../utils/constants';
13
+
14
+ // Actions
15
+ import { success } from '../../../../redux/actions/snackbarActions';
16
+
17
+ // Components
18
+ import DialogV2 from '../../../../components/GenericUIBlocks/Dialog/V2';
19
+
20
+ // Icons
21
+ // @ts-ignore
22
+ import Upload from '../../../../assets/images/templates/upload-image.svg';
23
+ // @ts-ignore
24
+ import Trash from '../../../../assets/images/templates/trash-upload.svg';
25
+ // @ts-ignore
26
+ import Download from '../../../../assets/images/templates/download.svg';
27
+ // @ts-ignore
28
+ import ConfirmCloseIcon from '../../../../assets/images/modal-icons/confirm-close-icon';
29
+
30
+ // Styles
31
+ import './styles.scss';
32
+
33
+ interface BrandingImage {
34
+ id: string | number;
35
+ url: string;
36
+ type: string;
37
+ name?: string;
38
+ meta?: any;
39
+ }
40
+
41
+ interface CustomUploadsV2Props {
42
+ store: StoreType;
43
+ onGetBrandingImages?: (payload: any) => Promise<any>;
44
+ onDeleteBrandingImage?: (id: string | number) => Promise<void>;
45
+ onUploadBrandingImage?: (payload: any) => Promise<any>;
46
+ }
47
+
48
+ const cancelDialogStylesV2 = {
49
+ maxWidth: '567px',
50
+ minHeight: 'auto',
51
+ padding: '40px',
52
+ };
53
+
54
+ export const CustomUploadsV2 = observer(({
55
+ store,
56
+ onGetBrandingImages,
57
+ onDeleteBrandingImage,
58
+ onUploadBrandingImage,
59
+ }: CustomUploadsV2Props) => {
60
+ const [images, setImages] = useState<BrandingImage[]>([]);
61
+ const [isLoading, setIsLoading] = useState(false);
62
+ const [isUploading, setIsUploading] = useState(false);
63
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
64
+ const [currentPage, setCurrentPage] = useState(1);
65
+ const [hasMore, setHasMore] = useState(true);
66
+ const [activeDropdown, setActiveDropdown] = useState<string | number | null>(null);
67
+ const [imageCache, setImageCache] = useState<Map<string, string>>(new Map());
68
+ const [openDeleteImage, setOpenDeleteImage] = useState(false);
69
+ const [selectedImage, setSelectedImage] = useState<BrandingImage | null>(null);
70
+
71
+ const fileInputRef = useRef<HTMLInputElement>(null);
72
+ const dropdownRefs = useRef<Map<string | number, HTMLDivElement>>(new Map());
73
+ const imagesGridRef = useRef<HTMLDivElement>(null);
74
+ const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
75
+ const isLoadingRef = useRef(false);
76
+ const dispatch: AppDispatch = useDispatch();
77
+
78
+ const loadBrandingImages = useCallback(async (page: number = 1, append: boolean = false, loading = true) => {
79
+ if (!onGetBrandingImages) return;
80
+
81
+ // Prevent multiple simultaneous calls
82
+ if (isLoadingRef.current) return;
83
+ isLoadingRef.current = true;
84
+
85
+ if (page === 1 && loading) {
86
+ setIsLoading(true);
87
+ } else {
88
+ setIsLoadingMore(true);
89
+ }
90
+
91
+ try {
92
+ const payload = {
93
+ page,
94
+ pageSize: 15
95
+ };
96
+
97
+ const brandingImages = await onGetBrandingImages(payload);
98
+
99
+ if (brandingImages && Array.isArray(brandingImages.rows)) {
100
+ const normalized = brandingImages.rows.map((img: any) => ({
101
+ id: img.id,
102
+ url: img.fileUrl,
103
+ type: img.type.toLowerCase(), // Convert to lowercase for consistency
104
+ name: img.name,
105
+ meta: img.meta
106
+ })) as BrandingImage[];
107
+
108
+ if (append && page > 1) {
109
+ // Merge with existing images, avoiding duplicates
110
+ setImages(prevImages => {
111
+ const existingIds = new Set(prevImages.map(img => img.id));
112
+ const newImages = normalized.filter(img => !existingIds.has(img.id));
113
+ return [...prevImages, ...newImages];
114
+ });
115
+ } else {
116
+ // Replace images for initial load
117
+ setImages(normalized);
118
+ }
119
+
120
+ // Update pagination state using API response pagination info
121
+ const hasMorePages = brandingImages.currentPage < brandingImages.lastPage;
122
+ setHasMore(hasMorePages);
123
+ setCurrentPage(brandingImages.currentPage);
124
+ } else {
125
+ if (!append) {
126
+ setImages([]);
127
+ }
128
+ setHasMore(false);
129
+ }
130
+ } catch (error) {
131
+ console.error('Failed to load branding images:', error);
132
+ } finally {
133
+ setIsLoading(false);
134
+ setIsLoadingMore(false);
135
+ isLoadingRef.current = false;
136
+ }
137
+ }, [onGetBrandingImages]);
138
+
139
+ // Handle loading more images on scroll
140
+ const loadMoreImages = useCallback(async () => {
141
+ if (!hasMore || isLoadingMore || isLoading) return;
142
+ await loadBrandingImages(currentPage + 1, true);
143
+ }, [hasMore, isLoadingMore, isLoading, currentPage, loadBrandingImages]);
144
+
145
+ // Debounced scroll handler
146
+ const handleScroll = useCallback(() => {
147
+ if (!imagesGridRef.current || !hasMore || isLoadingMore || isLoading) return;
148
+
149
+ const { scrollTop, scrollHeight, clientHeight } = imagesGridRef.current;
150
+ const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
151
+
152
+ // Load more when scrolled 80% of the way down
153
+ if (scrollPercentage > 0.75) {
154
+ // Clear existing timeout
155
+ if (loadingTimeoutRef.current) {
156
+ clearTimeout(loadingTimeoutRef.current);
157
+ }
158
+
159
+ // Debounce the API call by 300ms
160
+ loadingTimeoutRef.current = setTimeout(() => {
161
+ loadMoreImages();
162
+ }, 300);
163
+ }
164
+ }, [hasMore, isLoadingMore, isLoading, loadMoreImages]);
165
+
166
+ const handleOpenDeleteImage = () => {
167
+ setOpenDeleteImage(true);
168
+ setActiveDropdown(null);
169
+ };
170
+
171
+ const handleCloseDeleteImage = () => {
172
+ setOpenDeleteImage(false);
173
+ };
174
+
175
+ // Handle file validation
176
+ const validateFile = (file: File): boolean => {
177
+ if (!file || !allowedImageTypes.includes(file.type)) {
178
+ console.log('Only image files with extensions JPEG, PNG, or SVG are allowed.');
179
+ return false;
180
+ }
181
+ if (file.size >= 5 * 1024 * 1024) { // 5MB limit
182
+ console.log('File size must be under 5MB.');
183
+ return false;
184
+ }
185
+ return true;
186
+ };
187
+
188
+ const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
189
+ return new Promise((resolve, reject) => {
190
+ const reader = new FileReader();
191
+
192
+ reader.onload = (e) => {
193
+ const img = new Image();
194
+ img.onload = () => {
195
+ resolve({ width: img.width, height: img.height });
196
+ };
197
+ img.onerror = reject;
198
+ img.src = e.target?.result as string;
199
+ };
200
+
201
+ reader.onerror = reject;
202
+ reader.readAsDataURL(file);
203
+ });
204
+ };
205
+
206
+ // Handle file upload
207
+ const handleFileUpload = async (files: FileList) => {
208
+ if (!files.length) return;
209
+
210
+ setIsUploading(true);
211
+ try {
212
+ for (const file of Array.from(files)) {
213
+ if (!validateFile(file)) {
214
+ continue;
215
+ }
216
+
217
+ if (onUploadBrandingImage) {
218
+ const formData = new FormData();
219
+ let { width, height } = await getImageDimensions(file);
220
+ const metaData = {
221
+ width, height
222
+ }
223
+ formData.append('image', file);
224
+ formData.append('meta_data', JSON.stringify(metaData));
225
+ await onUploadBrandingImage(formData);
226
+ // Reset pagination and reload from first page
227
+ setCurrentPage(1);
228
+ setHasMore(true);
229
+ await loadBrandingImages(1, false, false);
230
+ } else {
231
+ const localUrl = URL.createObjectURL(file);
232
+ const type = getType(file);
233
+
234
+ const tempImage: BrandingImage = {
235
+ id: `temp-${Date.now()}-${Math.random()}`,
236
+ url: localUrl,
237
+ type,
238
+ name: file.name,
239
+ };
240
+
241
+ setImages((prev) => [...prev, tempImage]);
242
+
243
+ // Cache the image URL
244
+ setImageCache(
245
+ (prev) => new Map(prev.set(tempImage.url, tempImage.url))
246
+ );
247
+ }
248
+ }
249
+ } catch (error) {
250
+ console.error('Upload failed:', error);
251
+ } finally {
252
+ setIsUploading(false);
253
+ if (fileInputRef.current) {
254
+ fileInputRef.current.value = '';
255
+ }
256
+ }
257
+ };
258
+
259
+ // Handle delete image
260
+ const handleDeleteImage = async (imageId: string | number) => {
261
+ if (!onDeleteBrandingImage) return;
262
+
263
+ try {
264
+ await onDeleteBrandingImage(imageId);
265
+ setImages(prev => prev.filter(img => img.id !== imageId));
266
+ setActiveDropdown(null);
267
+
268
+ // Clean up cache
269
+ const imageToDelete = images.find(img => img.id === imageId);
270
+ if (imageToDelete?.url.startsWith('blob:')) {
271
+ URL.revokeObjectURL(imageToDelete.url);
272
+ }
273
+ setImageCache(prev => {
274
+ const newCache = new Map(prev);
275
+ newCache.delete(imageToDelete?.url || '');
276
+ return newCache;
277
+ });
278
+ } catch (error) {
279
+ console.error('Failed to delete image:', error);
280
+ }
281
+ };
282
+
283
+ // Handle download image
284
+ const handleDownloadImage = async (image: BrandingImage) => {
285
+ try {
286
+ const response = await fetch(image.url, {
287
+ method: 'GET',
288
+ headers: {
289
+ Authorization: `Bearer ${getPublicApiKey()}`,
290
+ },
291
+ });
292
+
293
+ if (!response.ok) throw new Error('Failed to download image');
294
+
295
+ // Convert response to Blob
296
+ const blob = await response.blob();
297
+
298
+ // Create a temporary object URL for download
299
+ const url = window.URL.createObjectURL(blob);
300
+ const link = document.createElement('a');
301
+ link.href = url;
302
+ link.download = image.name || `branding-image-${image.id}.png`;
303
+ document.body.appendChild(link);
304
+ link.click();
305
+
306
+ // Clean up
307
+ document.body.removeChild(link);
308
+ window.URL.revokeObjectURL(url);
309
+ setActiveDropdown(null);
310
+ } catch (error) {
311
+ console.error('Download failed:', error);
312
+ }
313
+
314
+ };
315
+
316
+ // Handle image selection for canvas
317
+ const handleImageSelect = async (image: BrandingImage, pos?: { x: number; y: number }, element?: any) => {
318
+ const imageUrl = image.url;
319
+ const type = image.type;
320
+
321
+ const imageMetaData = typeof image?.meta?.data === 'string' ? JSON.parse(image?.meta?.data) : image?.meta?.data || {};
322
+ let width = imageMetaData?.width || 100;
323
+ let height = imageMetaData?.height || 100;
324
+
325
+ if (element && element.type === 'svg' && element.contentEditable && type === 'image') {
326
+ element.set({ maskSrc: imageUrl });
327
+ return;
328
+ }
329
+
330
+ if (element && element.type === 'image' && element.contentEditable && type === 'image') {
331
+ const crop = getCrop(element, { width, height });
332
+ element.set({ src: imageUrl, ...crop });
333
+ return;
334
+ }
335
+
336
+ const scale = Math.min(store.width / width, store.height / height, 1);
337
+ width = width * scale;
338
+ height = height * scale;
339
+
340
+ const x = (pos?.x || store.width / 2) - width / 2;
341
+ const y = (pos?.y || store.height / 2) - height / 2;
342
+
343
+ store.activePage?.addElement({
344
+ type: type === 'svg' ? type : 'image',
345
+ //@ts-ignore
346
+ src: imageUrl,
347
+ x,
348
+ y,
349
+ width,
350
+ height,
351
+ });
352
+ };
353
+
354
+ // Handle dropdown toggle
355
+ const toggleDropdown = (imageId: string | number, event: React.MouseEvent) => {
356
+ event.stopPropagation();
357
+ setActiveDropdown(activeDropdown === imageId ? null : imageId);
358
+ };
359
+
360
+ // Close dropdown when clicking outside
361
+ useEffect(() => {
362
+ const handleClickOutside = (event: MouseEvent) => {
363
+ if (activeDropdown) {
364
+ const dropdownElement = dropdownRefs.current.get(activeDropdown);
365
+ if (dropdownElement && !dropdownElement.contains(event.target as Node)) {
366
+ setActiveDropdown(null);
367
+ }
368
+ }
369
+ };
370
+
371
+ document.addEventListener('mousedown', handleClickOutside);
372
+ return () => document.removeEventListener('mousedown', handleClickOutside);
373
+ }, [activeDropdown]);
374
+
375
+ // Load images on mount
376
+ useEffect(() => {
377
+ loadBrandingImages(1, false);
378
+ }, []);
379
+
380
+ // Setup scroll event listener
381
+ useEffect(() => {
382
+ const gridElement = imagesGridRef.current;
383
+ if (gridElement) {
384
+ gridElement.addEventListener('scroll', handleScroll);
385
+ return () => {
386
+ gridElement.removeEventListener('scroll', handleScroll);
387
+ // Clean up timeout on unmount
388
+ if (loadingTimeoutRef.current) {
389
+ clearTimeout(loadingTimeoutRef.current);
390
+ }
391
+ };
392
+ }
393
+ }, [hasMore, isLoadingMore, isLoading, currentPage, handleScroll]);
394
+
395
+ // Cleanup blob URLs on unmount
396
+ useEffect(() => {
397
+ return () => {
398
+ images.forEach(image => {
399
+ if (image.url.startsWith('blob:')) {
400
+ URL.revokeObjectURL(image.url);
401
+ }
402
+ });
403
+ };
404
+ }, []);
405
+
406
+ return (
407
+ <div className="custom-uploads-v2">
408
+
409
+ {/* Upload Section */}
410
+ <div className="upload-section">
411
+ <input
412
+ ref={fileInputRef}
413
+ type="file"
414
+ accept=".jpg,.jpeg,.png,.svg"
415
+ multiple
416
+ className="file-input"
417
+ onChange={(e) => e.target.files && handleFileUpload(e.target.files)}
418
+ />
419
+
420
+ <button
421
+ className={`upload-button ${isUploading ? 'uploading' : ''}`}
422
+ onClick={() => fileInputRef.current?.click()}
423
+ disabled={isUploading}
424
+ >
425
+ <img src={Upload} alt="upload" />
426
+ <span className="button-text">
427
+ {isUploading ? 'Uploading...' : 'Upload Image'}
428
+ </span>
429
+ </button>
430
+ </div>
431
+
432
+ {/* Header */}
433
+ <div className="upload-header">
434
+ <div className="upload-constraints">
435
+ <div className="format-text">
436
+ Accepted File Formats: JPEG, PNG, SVG
437
+ </div>
438
+ <div className="size-text">
439
+ Max Size: 5MB
440
+ </div>
441
+ <div className="quality-note">
442
+ Use high-res images for best print quality.
443
+ </div>
444
+ </div>
445
+ </div>
446
+
447
+ {/* Images Grid */}
448
+ <div className="images-grid" ref={imagesGridRef}>
449
+ {isLoading ? (
450
+ <div className="loading-state">Loading images...</div>
451
+ ) : images.length === 0 ? (
452
+ <div className="empty-state">No images uploaded yet</div>
453
+ ) : (
454
+ <>
455
+ {images.map((image) => (
456
+ <div key={image.id} className="image-item">
457
+ <div
458
+ className="image-wrapper"
459
+ onClick={() => handleImageSelect(image)}
460
+ >
461
+ <img
462
+ src={image.url}
463
+ alt={image.name || 'Branding image'}
464
+ className="image-preview"
465
+ loading="lazy"
466
+ />
467
+ <div
468
+ className="image-menu"
469
+ onClick={(e) => toggleDropdown(image.id, e)}
470
+ >
471
+ <div className="menu-dots">
472
+ <span></span>
473
+ <span></span>
474
+ <span></span>
475
+ </div>
476
+ </div>
477
+ </div>
478
+
479
+ {/* Dropdown Menu */}
480
+ {activeDropdown === image.id && (
481
+ <div
482
+ ref={(el) => {
483
+ if (el) dropdownRefs.current.set(image.id, el);
484
+ }}
485
+ className="dropdown-menu"
486
+ >
487
+ {onDeleteBrandingImage && <button
488
+ className="dropdown-item"
489
+ onClick={() => {
490
+ setSelectedImage(image);
491
+ handleOpenDeleteImage();
492
+ }}
493
+ disabled={!onDeleteBrandingImage}
494
+ >
495
+ <img src={Trash} alt="trash" />
496
+ Delete
497
+ </button>}
498
+ <button
499
+ className="dropdown-item"
500
+ onClick={() => handleDownloadImage(image)}
501
+ >
502
+ <img src={Download} alt="download" />
503
+ Download
504
+ </button>
505
+ </div>
506
+ )}
507
+ </div>
508
+ ))
509
+ }
510
+
511
+ {/* Loading indicator for pagination */}
512
+ {isLoadingMore && (
513
+ <div className="loading-more">
514
+ <div className="loading-state">Loading more images...</div>
515
+ </div>
516
+ )}
517
+ </>
518
+ )}
519
+
520
+ <DialogV2
521
+ icon={<ConfirmCloseIcon fill='var(--primary-color)' />}
522
+ customStyles={cancelDialogStylesV2}
523
+ open={openDeleteImage}
524
+ handleClose={handleCloseDeleteImage}
525
+ title='Delete Image'
526
+ subHeading=''
527
+ description='Are you sure you want to delete this image?'
528
+ onSubmit={() => {
529
+ handleDeleteImage(selectedImage?.id as string | number);
530
+ handleCloseDeleteImage();
531
+ }}
532
+ onCancel={handleCloseDeleteImage}
533
+ cancelText='No'
534
+ submitText='Yes'
535
+ isGallery={false}
536
+ />
537
+ </div>
538
+ </div>
539
+ );
540
+ });
541
+
542
+ export default CustomUploadsV2;