@griddo/ax 11.14.2-rc.0 → 11.14.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 (148) hide show
  1. package/config/jest/reactEasyCropMock.js +15 -0
  2. package/config/jest/reactTimezoneMock.js +13 -0
  3. package/package.json +221 -219
  4. package/public/img/welcome.svg +127 -0
  5. package/src/__tests__/components/Browser/Browser.test.tsx +27 -51
  6. package/src/__tests__/components/CategoryCell/CategoryCell.test.tsx +10 -5
  7. package/src/__tests__/components/ElementsTooltip/ElementsTooltip.test.tsx +27 -14
  8. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +2 -0
  9. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +138 -1
  10. package/src/__tests__/components/ImageDragAndDrop/CropStep/CropStep.test.tsx +84 -0
  11. package/src/__tests__/components/ImageDragAndDrop/ImageDragAndDrop.test.tsx +173 -0
  12. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +3 -4
  13. package/src/__tests__/components/ProfileImage/ProfileImage.test.tsx +120 -0
  14. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +8 -0
  15. package/src/__tests__/components/UserRolesAndSites/RoleItem/RoleItem.test.tsx +190 -0
  16. package/src/__tests__/components/UserRolesAndSites/UserRolesAndSites.test.tsx +471 -0
  17. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +15 -2
  18. package/src/__tests__/modules/Sites/Sites.test.tsx +68 -224
  19. package/src/__tests__/modules/Sites/SitesList/ListView/BulkHeader/BulkHeader.test.tsx +21 -17
  20. package/src/__tests__/modules/Sites/SitesList/SitesList.test.tsx +65 -565
  21. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/DataStep/DataStep.test.tsx +109 -0
  22. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/FinalStep/FinalStep.test.tsx +157 -0
  23. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/CropView.test.tsx +51 -0
  24. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/ImageStep.test.tsx +70 -0
  25. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/UploadView.test.tsx +92 -0
  26. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/TimezoneStep/TimezoneStep.test.tsx +94 -0
  27. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeModal.test.tsx +78 -0
  28. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeStep/WelcomeStep.test.tsx +39 -0
  29. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/utils.test.ts +55 -0
  30. package/src/api/sites.tsx +4 -4
  31. package/src/components/Avatar/index.tsx +26 -5
  32. package/src/components/Avatar/style.tsx +20 -10
  33. package/src/components/Browser/index.tsx +7 -1
  34. package/src/components/ConfigPanel/index.tsx +11 -7
  35. package/src/components/ElementsTooltip/index.tsx +96 -34
  36. package/src/components/ElementsTooltip/style.tsx +12 -1
  37. package/src/components/Fields/FileField/index.tsx +16 -18
  38. package/src/components/Fields/HeadingField/index.tsx +1 -1
  39. package/src/components/Fields/ImageField/index.tsx +9 -38
  40. package/src/components/Fields/ImageField/style.tsx +12 -1
  41. package/src/components/Fields/ToggleField/index.tsx +1 -1
  42. package/src/components/Fields/Wysiwyg/index.tsx +25 -20
  43. package/src/components/FileGallery/GalleryPanel/index.tsx +15 -7
  44. package/src/components/FileGallery/index.tsx +33 -28
  45. package/src/components/Gallery/GalleryPanel/index.tsx +5 -16
  46. package/src/components/Gallery/index.tsx +0 -2
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +11 -2
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +21 -3
  49. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +2 -2
  50. package/src/components/HeadingsPreviewModal/index.tsx +13 -3
  51. package/src/components/HeadingsPreviewModal/style.tsx +18 -0
  52. package/src/components/HeadingsPreviewModal/utils.tsx +31 -3
  53. package/src/components/Image/index.tsx +2 -2
  54. package/src/components/ImageDragAndDrop/CropStep/index.tsx +95 -0
  55. package/src/components/ImageDragAndDrop/CropStep/style.tsx +101 -0
  56. package/src/{modules/MediaGallery → components}/ImageDragAndDrop/index.tsx +103 -40
  57. package/src/{modules/MediaGallery → components}/ImageDragAndDrop/style.tsx +14 -2
  58. package/src/components/KeywordsPreviewModal/atoms.tsx +2 -2
  59. package/src/components/KeywordsPreviewModal/index.tsx +6 -6
  60. package/src/components/KeywordsPreviewModal/utils.tsx +2 -2
  61. package/src/components/ProfileImage/index.tsx +55 -0
  62. package/src/components/ProfileImage/style.tsx +58 -0
  63. package/src/components/ResizePanel/ResizeHandle/index.tsx +44 -6
  64. package/src/components/ResizePanel/ResizeHandle/style.tsx +7 -0
  65. package/src/components/ResizePanel/index.tsx +25 -4
  66. package/src/components/Tabs/style.tsx +1 -1
  67. package/src/components/Tag/index.tsx +0 -1
  68. package/src/components/UserRolesAndSites/RoleItem/index.tsx +42 -0
  69. package/src/components/UserRolesAndSites/RoleItem/style.tsx +29 -0
  70. package/src/components/UserRolesAndSites/index.tsx +102 -0
  71. package/src/components/UserRolesAndSites/style.tsx +67 -0
  72. package/src/components/index.tsx +6 -0
  73. package/src/constants/index.ts +13 -1
  74. package/src/containers/App/actions.tsx +8 -1
  75. package/src/containers/Sites/actions.tsx +26 -0
  76. package/src/containers/Sites/constants.tsx +1 -0
  77. package/src/containers/Sites/interfaces.tsx +6 -0
  78. package/src/containers/Sites/reducer.tsx +5 -1
  79. package/src/containers/Users/reducer.tsx +6 -5
  80. package/src/guards/routeLeaving/index.tsx +9 -11
  81. package/src/helpers/images.tsx +50 -3
  82. package/src/helpers/index.tsx +2 -1
  83. package/src/hooks/forms.tsx +45 -48
  84. package/src/hooks/index.tsx +2 -1
  85. package/src/hooks/modals.tsx +4 -3
  86. package/src/hooks/window.ts +50 -2
  87. package/src/modules/ActivityLog/ItemLogUser/UserItem/index.tsx +1 -1
  88. package/src/modules/App/Routing/Logout/index.tsx +3 -5
  89. package/src/modules/App/Routing/NavMenu/NavItem/index.tsx +73 -52
  90. package/src/modules/App/Routing/NavMenu/NavItem/style.tsx +21 -7
  91. package/src/modules/App/Routing/NavMenu/index.tsx +59 -54
  92. package/src/modules/App/Routing/NavMenu/style.tsx +13 -11
  93. package/src/modules/CreatePass/index.tsx +1 -1
  94. package/src/modules/FileDrive/FileDragAndDrop/index.tsx +11 -8
  95. package/src/modules/FileDrive/FileModal/index.tsx +8 -9
  96. package/src/modules/FileDrive/index.tsx +1 -18
  97. package/src/modules/Forms/FormEditor/index.tsx +1 -1
  98. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +22 -11
  99. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +1 -1
  100. package/src/modules/MediaGallery/ImageModal/index.tsx +1 -5
  101. package/src/modules/MediaGallery/index.tsx +1 -3
  102. package/src/modules/Settings/Globals/constants.tsx +942 -106
  103. package/src/modules/Sites/SitesList/AllSitesHeader/index.tsx +33 -0
  104. package/src/modules/Sites/SitesList/AllSitesHeader/style.tsx +35 -0
  105. package/src/modules/Sites/SitesList/GridView/GridHeaderFilter/index.tsx +5 -5
  106. package/src/modules/Sites/SitesList/GridView/GridSiteItem/index.tsx +23 -119
  107. package/src/modules/Sites/SitesList/ListView/BulkHeader/TableHeader/index.tsx +4 -4
  108. package/src/modules/Sites/SitesList/ListView/BulkHeader/index.tsx +4 -3
  109. package/src/modules/Sites/SitesList/ListView/ListSiteItem/index.tsx +23 -120
  110. package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/index.tsx +4 -5
  111. package/src/modules/Sites/SitesList/RecentSites/index.tsx +49 -0
  112. package/src/modules/Sites/SitesList/RecentSites/style.tsx +92 -0
  113. package/src/modules/Sites/SitesList/SiteModal/index.tsx +8 -7
  114. package/src/modules/Sites/SitesList/WelcomeModal/DataStep/index.tsx +72 -0
  115. package/src/modules/Sites/SitesList/WelcomeModal/DataStep/style.tsx +59 -0
  116. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/constants.tsx +78 -0
  117. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/index.tsx +78 -0
  118. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/style.tsx +141 -0
  119. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/index.tsx +93 -0
  120. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/style.tsx +77 -0
  121. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/index.tsx +100 -0
  122. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/style.tsx +94 -0
  123. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/index.tsx +44 -0
  124. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/style.tsx +31 -0
  125. package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/index.tsx +51 -0
  126. package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/style.tsx +52 -0
  127. package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/index.tsx +40 -0
  128. package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/style.tsx +53 -0
  129. package/src/modules/Sites/SitesList/WelcomeModal/index.tsx +215 -0
  130. package/src/modules/Sites/SitesList/WelcomeModal/style.tsx +12 -0
  131. package/src/modules/Sites/SitesList/WelcomeModal/utils.ts +26 -0
  132. package/src/modules/Sites/SitesList/atoms.tsx +4 -4
  133. package/src/modules/Sites/SitesList/hooks.tsx +149 -16
  134. package/src/modules/Sites/SitesList/index.tsx +127 -125
  135. package/src/modules/Sites/SitesList/style.tsx +1 -117
  136. package/src/modules/Sites/SitesList/utils.tsx +9 -2
  137. package/src/modules/Sites/index.tsx +19 -8
  138. package/src/modules/Users/Profile/index.tsx +169 -31
  139. package/src/modules/Users/Profile/style.tsx +81 -1
  140. package/src/modules/Users/Roles/RoleItem/index.tsx +2 -2
  141. package/src/modules/Users/UserCreate/SiteItem/index.tsx +11 -14
  142. package/src/modules/Users/UserForm/atoms.tsx +3 -3
  143. package/src/modules/Users/UserForm/index.tsx +25 -29
  144. package/src/modules/Users/UserForm/style.tsx +15 -2
  145. package/src/modules/Users/UserList/UserItem/index.tsx +4 -4
  146. package/src/routes/index.tsx +1 -0
  147. package/src/types/index.tsx +2 -0
  148. /package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/style.tsx +0 -0
@@ -0,0 +1,101 @@
1
+ import styled from "styled-components";
2
+
3
+ const Wrapper = styled.div`
4
+ display: flex;
5
+ flex-direction: column;
6
+ width: 100%;
7
+ height: 100%;
8
+ justify-content: space-between;
9
+ `;
10
+
11
+ const CropArea = styled.div`
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ width: 100%;
16
+ `;
17
+
18
+ const CropContent = styled.div`
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 24px;
22
+ position: relative;
23
+ `;
24
+
25
+ const CropContainer = styled.div`
26
+ position: relative;
27
+ width: 236px;
28
+ height: 236px;
29
+ border-radius: 50%;
30
+ overflow: hidden;
31
+ flex-shrink: 0;
32
+ `;
33
+
34
+ const ZoomControls = styled.div`
35
+ display: flex;
36
+ flex-direction: column;
37
+ align-items: center;
38
+ gap: ${(p) => p.theme.spacing.xs};
39
+ position: absolute;
40
+ right: -48px;
41
+ top: 50%;
42
+ transform: translateY(-50%);
43
+ pointer-events: none;
44
+ `;
45
+
46
+ const ZoomButton = styled.button`
47
+ background: none;
48
+ border: none;
49
+ cursor: pointer;
50
+ color: ${(p) => p.theme.color.interactive01};
51
+ font-size: 24px;
52
+ line-height: 1;
53
+ padding: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ width: 24px;
58
+ height: 24px;
59
+ pointer-events: auto;
60
+
61
+ &:hover {
62
+ opacity: 0.7;
63
+ }
64
+ `;
65
+
66
+ const ZoomSlider = styled.input`
67
+ appearance: slider-vertical;
68
+ writing-mode: vertical-lr;
69
+ direction: rtl;
70
+ width: 4px;
71
+ height: 120px;
72
+ cursor: pointer;
73
+ accent-color: ${(p) => p.theme.color.interactive01};
74
+ pointer-events: auto;
75
+ `;
76
+
77
+ const CancelButtonContainer = styled.div`
78
+ display: flex;
79
+ justify-content: center;
80
+ width: 100%;
81
+ margin-top: ${(p) => p.theme.spacing.s};
82
+ `;
83
+
84
+ const Actions = styled.div`
85
+ display: flex;
86
+ gap: ${(p) => p.theme.spacing.m};
87
+ justify-content: flex-end;
88
+ width: 100%;
89
+ `;
90
+
91
+ export {
92
+ Wrapper,
93
+ CropArea,
94
+ CropContent,
95
+ CropContainer,
96
+ ZoomControls,
97
+ ZoomButton,
98
+ ZoomSlider,
99
+ CancelButtonContainer,
100
+ Actions,
101
+ };
@@ -1,15 +1,20 @@
1
- import React, { memo, useEffect, useRef, useState } from "react";
1
+ import type React from "react";
2
+ import { memo, useEffect, useRef, useState } from "react";
3
+ import type { Area } from "react-easy-crop";
2
4
  import { connect } from "react-redux";
3
5
 
4
- import { Icon, DragAndDrop, ProgressBar } from "@ax/components";
5
- import { IGetFolderParams, IImage, IRootState } from "@ax/types";
6
+ import { DragAndDrop, Icon, ProgressBar } from "@ax/components";
7
+ import { VALID_IMAGE_FORMATS } from "@ax/constants";
6
8
  import { galleryActions } from "@ax/containers/Gallery";
9
+ import { getCroppedImg } from "@ax/helpers";
10
+ import type { IGetFolderParams, IImage, IRootState } from "@ax/types";
11
+
12
+ import CropStep from "./CropStep";
7
13
 
8
14
  import * as S from "./style";
9
15
 
10
16
  const ImageDragAndDrop = (props: IProps) => {
11
17
  const {
12
- validFormats,
13
18
  isUploading,
14
19
  isSuccess,
15
20
  isError,
@@ -28,15 +33,18 @@ const ImageDragAndDrop = (props: IProps) => {
28
33
  visible = true,
29
34
  getParams,
30
35
  setUploadSuccess,
36
+ withCrop = false,
37
+ maxImages,
31
38
  } = props;
32
39
 
33
- const validExtensions = validFormats.map((format) => `.${format}`).join(",");
40
+ const validExtensions = VALID_IMAGE_FORMATS.map((format) => `.${format}`).join(",");
34
41
  const filesInputRef = useRef<HTMLInputElement | null>(null);
35
42
  const filesButtonRef = useRef<HTMLButtonElement | null>(null);
43
+ const dropDepthRef = useRef(0);
36
44
  const [inDropZone, setInDropZone] = useState(false);
37
- const [dropDepth, setDropDepth] = useState(0);
38
45
  const [uploadingState, setUploadingState] = useState({ total: 0, ready: 0 });
39
46
  const [progress, setProgress] = useState(0);
47
+ const [cropImageSrc, setCropImageSrc] = useState<string | null>(null);
40
48
 
41
49
  const uploading = isUploading || uploadingState.total > uploadingState.ready;
42
50
  const success = isSuccess && uploadingState.total === uploadingState.ready;
@@ -46,13 +54,15 @@ const ImageDragAndDrop = (props: IProps) => {
46
54
  }, [setUploadSuccess]);
47
55
 
48
56
  const handleDragEnter = () => {
49
- setDropDepth((depth) => depth + 1);
57
+ dropDepthRef.current++;
58
+ setInDropZone(true);
50
59
  };
51
60
 
52
61
  const handleDragLeave = () => {
53
- setDropDepth((depth) => depth - 1);
54
- if (dropDepth > 1) return;
55
- setInDropZone(false);
62
+ dropDepthRef.current--;
63
+ if (dropDepthRef.current === 0) {
64
+ setInDropZone(false);
65
+ }
56
66
  };
57
67
 
58
68
  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
@@ -60,44 +70,70 @@ const ImageDragAndDrop = (props: IProps) => {
60
70
  setInDropZone(true);
61
71
  };
62
72
 
63
- const checkType = (fileName: string) => {
64
- const fileNameArray = fileName.split(".");
65
- if (validFormats.includes(fileNameArray[fileNameArray.length - 1])) {
66
- return true;
67
- }
68
- return false;
73
+ const isValidFileType = (fileName: string): boolean => {
74
+ const extension = fileName.split(".").pop()?.toLowerCase();
75
+ return extension ? VALID_IMAGE_FORMATS.includes(extension) : false;
69
76
  };
70
77
 
71
- const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
72
- const files = Array.from(e.dataTransfer.files);
73
- if (handleMultipleUpload && files.length > 1) {
74
- if (!files.every((file) => checkType(file.name))) {
75
- uploadError(true, "Invalid format");
76
- } else {
77
- handleMultipleUpload(files);
78
- }
79
- } else {
80
- await uploadFiles(files);
81
- }
82
- setDropDepth(0);
78
+ const validateFiles = (files: File[]): boolean => {
79
+ return files.every((file) => isValidFileType(file.name));
83
80
  };
84
81
 
85
- const handleFilesUpload = async (e: any) => {
86
- const files: File[] = Array.from(e.currentTarget.files);
82
+ const readFileAsDataUrl = (file: File): Promise<string> =>
83
+ new Promise((resolve, reject) => {
84
+ const reader = new FileReader();
85
+ reader.onload = (e) => {
86
+ const result = e.target?.result;
87
+ if (typeof result === "string") resolve(result);
88
+ else reject(new Error("Failed to read file"));
89
+ };
90
+ reader.onerror = () => reject(new Error("Failed to read file"));
91
+ reader.readAsDataURL(file);
92
+ });
93
+
94
+ const handleUploadAction = async (files: File[]) => {
95
+ if (maxImages && files.length > maxImages) {
96
+ uploadError(true, `Maximum ${maxImages} image${maxImages !== 1 ? "s" : ""} allowed`);
97
+ return;
98
+ }
99
+
87
100
  if (handleMultipleUpload && files.length > 1) {
88
- if (!files.every((file) => checkType(file.name))) {
101
+ if (!validateFiles(files)) {
89
102
  uploadError(true, "Invalid format");
90
103
  } else {
91
104
  handleMultipleUpload(files);
92
105
  }
106
+ } else if (withCrop && files.length === 1) {
107
+ if (!validateFiles(files)) {
108
+ uploadError(true, "Invalid format");
109
+ return;
110
+ }
111
+ try {
112
+ const dataUrl = await readFileAsDataUrl(files[0]);
113
+ setCropImageSrc(dataUrl);
114
+ } catch (_error) {
115
+ uploadError(true, "Failed to load image");
116
+ }
93
117
  } else {
94
- await uploadFiles(files);
118
+ uploadFiles(files);
95
119
  }
96
120
  };
97
121
 
122
+ const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
123
+ const files = Array.from(e.dataTransfer.files);
124
+ handleUploadAction(files);
125
+ dropDepthRef.current = 0;
126
+ setInDropZone(false);
127
+ };
128
+
129
+ const handleFilesUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
130
+ const files: File[] = Array.from(e.currentTarget.files || []);
131
+ handleUploadAction(files);
132
+ };
133
+
98
134
  const uploadFiles = async (files: File[]) => {
99
135
  try {
100
- if (!files.every((file) => checkType(file.name))) {
136
+ if (!validateFiles(files)) {
101
137
  uploadError(true, "Invalid format");
102
138
  return;
103
139
  }
@@ -132,7 +168,8 @@ const ImageDragAndDrop = (props: IProps) => {
132
168
  }, delay);
133
169
  }
134
170
  } catch (error) {
135
- console.log(error);
171
+ uploadError(true, "Upload failed. Please try again.");
172
+ console.error("Upload error:", error);
136
173
  }
137
174
  };
138
175
 
@@ -142,6 +179,21 @@ const ImageDragAndDrop = (props: IProps) => {
142
179
  resetError();
143
180
  };
144
181
 
182
+ const handleCropConfirm = async (croppedAreaPixels: Area) => {
183
+ if (!cropImageSrc) return;
184
+ try {
185
+ const croppedFile = await getCroppedImg(cropImageSrc, croppedAreaPixels);
186
+ setCropImageSrc(null);
187
+ uploadFiles([croppedFile]);
188
+ } catch (_error) {
189
+ uploadError(true, "Failed to crop image");
190
+ }
191
+ };
192
+
193
+ const handleCropCancel = () => {
194
+ setCropImageSrc(null);
195
+ };
196
+
145
197
  const handleFileClick = () => {
146
198
  if (filesInputRef) {
147
199
  filesInputRef.current?.click();
@@ -156,7 +208,7 @@ const ImageDragAndDrop = (props: IProps) => {
156
208
  onDragOver={handleDragOver}
157
209
  onDragEnter={handleDragEnter}
158
210
  onDragLeave={handleDragLeave}
159
- validFormats={validFormats}
211
+ validFormats={VALID_IMAGE_FORMATS}
160
212
  >
161
213
  <S.StatusWrapper onDragEnter={handleDragEnter} onDragLeave={handleDragLeave}>
162
214
  <S.DragStatus onDragEnter={handleDragEnter} onDragLeave={handleDragLeave}>
@@ -165,7 +217,13 @@ const ImageDragAndDrop = (props: IProps) => {
165
217
  </S.DragIcon>
166
218
  <S.DragTitle>Drag your image here</S.DragTitle>
167
219
  <S.DragSubtitle>or</S.DragSubtitle>
168
- <S.FilesInput type="file" ref={filesInputRef} multiple accept={validExtensions} onInput={handleFilesUpload} />
220
+ <S.FilesInput
221
+ type="file"
222
+ ref={filesInputRef}
223
+ multiple={!maxImages || maxImages > 1}
224
+ accept={validExtensions}
225
+ onInput={handleFilesUpload}
226
+ />
169
227
  <S.FilesButton
170
228
  ref={filesButtonRef}
171
229
  type="button"
@@ -176,7 +234,7 @@ const ImageDragAndDrop = (props: IProps) => {
176
234
  Select images
177
235
  </S.FilesButton>
178
236
  <S.DragSubtitle>
179
- Valid formats: {validFormats.join(", ")}.
237
+ Valid formats: {VALID_IMAGE_FORMATS.join(", ")}.
180
238
  <br />
181
239
  Max. size: 50MB
182
240
  </S.DragSubtitle>
@@ -187,7 +245,7 @@ const ImageDragAndDrop = (props: IProps) => {
187
245
  </S.DropIcon>
188
246
  <S.DragTitle>Drop your image</S.DragTitle>
189
247
  <S.DragSubtitle>
190
- Valid formats: {validFormats.join(", ")}.
248
+ Valid formats: {VALID_IMAGE_FORMATS.join(", ")}.
191
249
  <br />
192
250
  Max. size: 50MB
193
251
  </S.DragSubtitle>
@@ -213,9 +271,13 @@ const ImageDragAndDrop = (props: IProps) => {
213
271
  success={success}
214
272
  error={isError}
215
273
  inverse={inverse}
274
+ cropping={!!cropImageSrc}
216
275
  >
217
276
  {isAllowedToUpload ? renderDragAndDrop() : renderPlaceholder()}
218
277
  </S.DragAndDropWrapper>
278
+ <S.CropWrapper cropping={!!cropImageSrc}>
279
+ {cropImageSrc && <CropStep imageSrc={cropImageSrc} onConfirm={handleCropConfirm} onCancel={handleCropCancel} />}
280
+ </S.CropWrapper>
219
281
  <S.UploadingWrapper
220
282
  inDropZone={inDropZone}
221
283
  uploading={uploading}
@@ -226,7 +288,7 @@ const ImageDragAndDrop = (props: IProps) => {
226
288
  <S.StatusWrapper>
227
289
  <S.UploadingStatus>
228
290
  <S.DragIcon>
229
- <Icon name="page" size="48" />
291
+ <Icon name="image" size="48" />
230
292
  </S.DragIcon>
231
293
  <S.ProgressBar>
232
294
  <ProgressBar percentage={progress} inverse={inverse} />
@@ -256,7 +318,6 @@ const ImageDragAndDrop = (props: IProps) => {
256
318
  };
257
319
 
258
320
  interface IProps {
259
- validFormats: string[];
260
321
  isUploading: boolean;
261
322
  isSuccess: boolean;
262
323
  isError: boolean;
@@ -267,6 +328,8 @@ interface IProps {
267
328
  isAllowedToUpload?: boolean;
268
329
  replaceData?: { fileID: number };
269
330
  visible?: boolean;
331
+ withCrop?: boolean;
332
+ maxImages?: number;
270
333
  handleUpload: (result: IImage[]) => void;
271
334
  handleMultipleUpload?: (files: File[]) => void;
272
335
  uploadError: (error: boolean, msg?: string) => Promise<void>;
@@ -114,6 +114,7 @@ const DragAndDropWrapper = styled.div<{
114
114
  success: boolean;
115
115
  error: boolean;
116
116
  inverse: boolean;
117
+ cropping: boolean;
117
118
  }>`
118
119
  border: ${(p) =>
119
120
  `2px dashed ${p.inDropZone || p.inverse ? p.theme.color.interactiveInverse : p.theme.color.interactive01}`};
@@ -122,8 +123,8 @@ const DragAndDropWrapper = styled.div<{
122
123
  p.inDropZone ? p.theme.color.interactive01 : p.inverse ? "transparent" : p.theme.color.uiBarBackground};
123
124
  width: 100%;
124
125
  height: 100%;
125
- opacity: ${(p) => (p.uploading || p.success || p.error ? "0" : "1")};
126
- display: ${(p) => (p.uploading || p.success || p.error ? "none" : "block")};
126
+ opacity: ${(p) => (p.uploading || p.success || p.error || p.cropping ? "0" : "1")};
127
+ display: ${(p) => (p.uploading || p.success || p.error || p.cropping ? "none" : "block")};
127
128
  transition: opacity 0.1s;
128
129
 
129
130
  ${DragStatus} {
@@ -237,6 +238,16 @@ const ProgressBar = styled.div`
237
238
  margin-bottom: ${(p) => p.theme.spacing.xs};
238
239
  `;
239
240
 
241
+ const CropWrapper = styled.div<{
242
+ cropping: boolean;
243
+ }>`
244
+ width: 100%;
245
+ height: 100%;
246
+ opacity: ${(p) => (p.cropping ? "1" : "0")};
247
+ display: ${(p) => (p.cropping ? "block" : "none")};
248
+ transition: opacity 0.1s;
249
+ `;
250
+
240
251
  export {
241
252
  Wrapper,
242
253
  StatusWrapper,
@@ -257,4 +268,5 @@ export {
257
268
  FilesInput,
258
269
  FilesButton,
259
270
  ProgressBar,
271
+ CropWrapper,
260
272
  };
@@ -1,7 +1,7 @@
1
1
  import { useState } from "react";
2
2
 
3
- import { FieldsBehavior, Modal } from "@ax/components";
4
3
  import type { IModal } from "@ax/types";
4
+ import { Modal, FieldsBehavior } from "@ax/components";
5
5
 
6
6
  import * as S from "./style";
7
7
 
@@ -39,7 +39,7 @@ const AddKeywordsModal = (props: IAddKeywordsModal) => {
39
39
  secondaryAction={secondaryModalAction}
40
40
  mainAction={mainModalAction}
41
41
  size="S"
42
- height={288}
42
+ height={282}
43
43
  >
44
44
  <S.ModalContent>
45
45
  <FieldsBehavior
@@ -69,16 +69,16 @@ const KeywordsPreviewModal = (props: IKeywordsPreviewProps) => {
69
69
  )}
70
70
  <S.KeywordsListWrapper>
71
71
  {keywords.length === 0 && <S.StyledSummaryButton />}
72
- {keywords.map((keyword, index) => {
73
- const isSelected = keywordsFilter.includes(keyword);
72
+ {Object.keys(keywordCounts).map((key, index) => {
73
+ const isSelected = keywordsFilter.includes(key);
74
74
  return (
75
75
  <KeywordItem
76
- keyword={keyword}
77
- count={keywordCounts[keyword] ?? 0}
76
+ keyword={key}
77
+ count={keywordCounts[key]}
78
78
  isSelected={isSelected}
79
- onClick={handleAddTag(keyword)}
79
+ onClick={handleAddTag(key)}
80
80
  deleteKeyword={handleDeleteKeyword}
81
- key={`${keyword}-${index}`}
81
+ key={`${key}-${index}`}
82
82
  />
83
83
  );
84
84
  })}
@@ -6,11 +6,11 @@ const countKeywords = (html: HTMLDivElement, keywords: string[]) => {
6
6
  return {};
7
7
  }
8
8
 
9
- const htmlContent = frameContent.innerText.toLowerCase().normalize("NFC");
9
+ const htmlContent = frameContent.innerText.toLowerCase();
10
10
  const keywordCounts: Record<string, number> = {};
11
11
 
12
12
  keywords.forEach((keyword) => {
13
- const lowerKeyword = keyword.toLowerCase().normalize("NFC");
13
+ const lowerKeyword = keyword.toLowerCase();
14
14
  const regex = new RegExp(lowerKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
15
15
  const matches = htmlContent.match(regex);
16
16
  keywordCounts[keyword] = matches ? matches.length : 0;
@@ -0,0 +1,55 @@
1
+ import { memo } from "react";
2
+
3
+ import { Icon, Image, ImageDragAndDrop, Modal } from "@ax/components";
4
+ import { useModal } from "@ax/hooks";
5
+ import type { IImage } from "@ax/types";
6
+
7
+ import * as S from "./style";
8
+
9
+ const ProfileImage = (props: IProfileImageProps) => {
10
+ const { imageUrl, size = 96, handleImage } = props;
11
+
12
+ const { isOpen, toggleModal } = useModal(false);
13
+
14
+ const handleUpload = (images: IImage[]) => {
15
+ handleImage(images[0]);
16
+ toggleModal();
17
+ };
18
+
19
+ return (
20
+ <>
21
+ <S.FieldWrapper size={size} data-testid="profile-image-wrapper" onClick={toggleModal}>
22
+ {imageUrl ? (
23
+ <S.ImageContainer>
24
+ <Image url={imageUrl} width={size} />
25
+ </S.ImageContainer>
26
+ ) : (
27
+ <S.DefaultImage>
28
+ <S.IconWrapper>
29
+ <Icon name="image" size="40" />
30
+ </S.IconWrapper>
31
+ </S.DefaultImage>
32
+ )}
33
+ <S.HoverOverlay>Edit Avatar</S.HoverOverlay>
34
+ </S.FieldWrapper>
35
+ <Modal isOpen={isOpen} hide={toggleModal} size="M" height={416} title="Upload Media">
36
+ <ImageDragAndDrop
37
+ siteID={"global"}
38
+ isAllowedToUpload={true}
39
+ handleUpload={handleUpload}
40
+ visible={false}
41
+ withCrop={true}
42
+ maxImages={1}
43
+ />
44
+ </Modal>
45
+ </>
46
+ );
47
+ };
48
+
49
+ export interface IProfileImageProps {
50
+ imageUrl?: string;
51
+ size?: number;
52
+ handleImage: (image: IImage) => void;
53
+ }
54
+
55
+ export default memo(ProfileImage);
@@ -0,0 +1,58 @@
1
+ import styled from "styled-components";
2
+
3
+ const FieldWrapper = styled.div<{ size: number }>`
4
+ width: ${(p) => `${p.size}px`};
5
+ height: ${(p) => `${p.size}px`};
6
+ border: ${(p) => `2px solid ${p.theme.color.uiLine}`};
7
+ border-radius: 50%;
8
+ position: relative;
9
+ cursor: pointer;
10
+
11
+ &:hover {
12
+ > div:last-child {
13
+ opacity: 1;
14
+ }
15
+ }
16
+ `;
17
+
18
+ const DefaultImage = styled.div`
19
+ display: flex;
20
+ justify-content: center;
21
+ align-items: center;
22
+ width: 100%;
23
+ height: 100%;
24
+ border-radius: 50%;
25
+ background-color: ${(p) => p.theme.color.uiBackground03};
26
+ `;
27
+
28
+ const IconWrapper = styled.div``;
29
+
30
+ const ImageContainer = styled.div`
31
+ display: flex;
32
+ justify-content: center;
33
+ align-items: center;
34
+ width: 100%;
35
+ height: 100%;
36
+ border-radius: 50%;
37
+ overflow: hidden;
38
+ `;
39
+
40
+ const HoverOverlay = styled.div`
41
+ position: absolute;
42
+ top: 0;
43
+ left: 0;
44
+ width: 100%;
45
+ height: 100%;
46
+ border-radius: 50%;
47
+ background-color: rgba(0, 27, 60, 0.5);
48
+ display: flex;
49
+ justify-content: center;
50
+ align-items: center;
51
+ color: white;
52
+ ${(p) => p.theme.textStyle.uiM};
53
+ font-weight: 600;
54
+ opacity: 0;
55
+ transition: opacity 0.3s ease;
56
+ `;
57
+
58
+ export { FieldWrapper, IconWrapper, DefaultImage, ImageContainer, HoverOverlay };
@@ -1,11 +1,15 @@
1
- import React, { useCallback, useEffect, useRef } from "react";
1
+ import { useCallback, useEffect, useRef } from "react";
2
2
 
3
3
  import * as S from "./style";
4
4
 
5
+ const MIN_WIDTH = 368;
6
+ const MAX_WIDTH = 1280;
7
+
5
8
  const ResizeHandle = (props: IResizeHandleProps): JSX.Element => {
6
- const { onMouseMove } = props;
9
+ const { onMouseMove, currentWidth = 500 } = props;
7
10
 
8
11
  const isDragging = useRef(false);
12
+ const handlerRef = useRef<HTMLDivElement>(null);
9
13
 
10
14
  const handleResize = useCallback(
11
15
  (e: MouseEvent) => {
@@ -13,7 +17,8 @@ const ResizeHandle = (props: IResizeHandleProps): JSX.Element => {
13
17
  e.preventDefault();
14
18
 
15
19
  const newWidth = document.body.offsetWidth - e.clientX;
16
- onMouseMove(newWidth);
20
+ const validatedWidth = Math.max(MIN_WIDTH, Math.min(newWidth, MAX_WIDTH));
21
+ onMouseMove(validatedWidth);
17
22
  },
18
23
  [onMouseMove],
19
24
  );
@@ -23,10 +28,30 @@ const ResizeHandle = (props: IResizeHandleProps): JSX.Element => {
23
28
  document.body.classList.remove("no-select");
24
29
  }, []);
25
30
 
26
- const handleMouseDown = () => {
31
+ const handleMouseDown = useCallback(() => {
27
32
  isDragging.current = true;
28
33
  document.body.classList.add("no-select");
29
- };
34
+ }, []);
35
+
36
+ const handleKeyDown = useCallback(
37
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
38
+ const step = 20;
39
+ let newWidth: number | null = null;
40
+
41
+ if (e.key === "ArrowRight") {
42
+ newWidth = Math.max(MIN_WIDTH, currentWidth - step);
43
+ e.preventDefault();
44
+ } else if (e.key === "ArrowLeft") {
45
+ newWidth = Math.min(MAX_WIDTH, currentWidth + step);
46
+ e.preventDefault();
47
+ }
48
+
49
+ if (newWidth !== null) {
50
+ onMouseMove(newWidth);
51
+ }
52
+ },
53
+ [onMouseMove, currentWidth],
54
+ );
30
55
 
31
56
  useEffect(() => {
32
57
  window.addEventListener("mousemove", handleResize);
@@ -56,11 +81,24 @@ const ResizeHandle = (props: IResizeHandleProps): JSX.Element => {
56
81
  };
57
82
  }, [handleResize]);
58
83
 
59
- return <S.Handler onMouseDown={handleMouseDown} data-testid="handler" />;
84
+ return (
85
+ <S.Handler
86
+ ref={handlerRef}
87
+ onMouseDown={handleMouseDown}
88
+ onKeyDown={handleKeyDown}
89
+ tabIndex={0}
90
+ role="slider"
91
+ aria-label="Panel resize handle"
92
+ aria-valuemin={MIN_WIDTH}
93
+ aria-valuemax={MAX_WIDTH}
94
+ data-testid="handler"
95
+ />
96
+ );
60
97
  };
61
98
 
62
99
  export interface IResizeHandleProps {
63
100
  onMouseMove: (value: number) => void;
101
+ currentWidth?: number;
64
102
  }
65
103
 
66
104
  export default ResizeHandle;
@@ -10,9 +10,16 @@ const Handler = styled.div`
10
10
  z-index: 1;
11
11
  transform: translateX(${(p) => p.theme.spacing.xs});
12
12
  flex-shrink: 0;
13
+ outline: none;
14
+ transition: background-color 0.2s ease;
15
+
13
16
  &:hover {
14
17
  background-color: ${(p) => p.theme.color.interactive01};
15
18
  }
19
+
20
+ &:focus-visible {
21
+ background-color: ${(p) => p.theme.color.interactive01};
22
+ }
16
23
  `;
17
24
 
18
25
  export { Handler };