@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.
- package/config/jest/reactEasyCropMock.js +15 -0
- package/config/jest/reactTimezoneMock.js +13 -0
- package/package.json +221 -219
- package/public/img/welcome.svg +127 -0
- package/src/__tests__/components/Browser/Browser.test.tsx +27 -51
- package/src/__tests__/components/CategoryCell/CategoryCell.test.tsx +10 -5
- package/src/__tests__/components/ElementsTooltip/ElementsTooltip.test.tsx +27 -14
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +2 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +138 -1
- package/src/__tests__/components/ImageDragAndDrop/CropStep/CropStep.test.tsx +84 -0
- package/src/__tests__/components/ImageDragAndDrop/ImageDragAndDrop.test.tsx +173 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +3 -4
- package/src/__tests__/components/ProfileImage/ProfileImage.test.tsx +120 -0
- package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +8 -0
- package/src/__tests__/components/UserRolesAndSites/RoleItem/RoleItem.test.tsx +190 -0
- package/src/__tests__/components/UserRolesAndSites/UserRolesAndSites.test.tsx +471 -0
- package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +15 -2
- package/src/__tests__/modules/Sites/Sites.test.tsx +68 -224
- package/src/__tests__/modules/Sites/SitesList/ListView/BulkHeader/BulkHeader.test.tsx +21 -17
- package/src/__tests__/modules/Sites/SitesList/SitesList.test.tsx +65 -565
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/DataStep/DataStep.test.tsx +109 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/FinalStep/FinalStep.test.tsx +157 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/CropView.test.tsx +51 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/ImageStep.test.tsx +70 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/UploadView.test.tsx +92 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/TimezoneStep/TimezoneStep.test.tsx +94 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeModal.test.tsx +78 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeStep/WelcomeStep.test.tsx +39 -0
- package/src/__tests__/modules/Sites/SitesList/WelcomeModal/utils.test.ts +55 -0
- package/src/api/sites.tsx +4 -4
- package/src/components/Avatar/index.tsx +26 -5
- package/src/components/Avatar/style.tsx +20 -10
- package/src/components/Browser/index.tsx +7 -1
- package/src/components/ConfigPanel/index.tsx +11 -7
- package/src/components/ElementsTooltip/index.tsx +96 -34
- package/src/components/ElementsTooltip/style.tsx +12 -1
- package/src/components/Fields/FileField/index.tsx +16 -18
- package/src/components/Fields/HeadingField/index.tsx +1 -1
- package/src/components/Fields/ImageField/index.tsx +9 -38
- package/src/components/Fields/ImageField/style.tsx +12 -1
- package/src/components/Fields/ToggleField/index.tsx +1 -1
- package/src/components/Fields/Wysiwyg/index.tsx +25 -20
- package/src/components/FileGallery/GalleryPanel/index.tsx +15 -7
- package/src/components/FileGallery/index.tsx +33 -28
- package/src/components/Gallery/GalleryPanel/index.tsx +5 -16
- package/src/components/Gallery/index.tsx +0 -2
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +11 -2
- package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +21 -3
- package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +2 -2
- package/src/components/HeadingsPreviewModal/index.tsx +13 -3
- package/src/components/HeadingsPreviewModal/style.tsx +18 -0
- package/src/components/HeadingsPreviewModal/utils.tsx +31 -3
- package/src/components/Image/index.tsx +2 -2
- package/src/components/ImageDragAndDrop/CropStep/index.tsx +95 -0
- package/src/components/ImageDragAndDrop/CropStep/style.tsx +101 -0
- package/src/{modules/MediaGallery → components}/ImageDragAndDrop/index.tsx +103 -40
- package/src/{modules/MediaGallery → components}/ImageDragAndDrop/style.tsx +14 -2
- package/src/components/KeywordsPreviewModal/atoms.tsx +2 -2
- package/src/components/KeywordsPreviewModal/index.tsx +6 -6
- package/src/components/KeywordsPreviewModal/utils.tsx +2 -2
- package/src/components/ProfileImage/index.tsx +55 -0
- package/src/components/ProfileImage/style.tsx +58 -0
- package/src/components/ResizePanel/ResizeHandle/index.tsx +44 -6
- package/src/components/ResizePanel/ResizeHandle/style.tsx +7 -0
- package/src/components/ResizePanel/index.tsx +25 -4
- package/src/components/Tabs/style.tsx +1 -1
- package/src/components/Tag/index.tsx +0 -1
- package/src/components/UserRolesAndSites/RoleItem/index.tsx +42 -0
- package/src/components/UserRolesAndSites/RoleItem/style.tsx +29 -0
- package/src/components/UserRolesAndSites/index.tsx +102 -0
- package/src/components/UserRolesAndSites/style.tsx +67 -0
- package/src/components/index.tsx +6 -0
- package/src/constants/index.ts +13 -1
- package/src/containers/App/actions.tsx +8 -1
- package/src/containers/Sites/actions.tsx +26 -0
- package/src/containers/Sites/constants.tsx +1 -0
- package/src/containers/Sites/interfaces.tsx +6 -0
- package/src/containers/Sites/reducer.tsx +5 -1
- package/src/containers/Users/reducer.tsx +6 -5
- package/src/guards/routeLeaving/index.tsx +9 -11
- package/src/helpers/images.tsx +50 -3
- package/src/helpers/index.tsx +2 -1
- package/src/hooks/forms.tsx +45 -48
- package/src/hooks/index.tsx +2 -1
- package/src/hooks/modals.tsx +4 -3
- package/src/hooks/window.ts +50 -2
- package/src/modules/ActivityLog/ItemLogUser/UserItem/index.tsx +1 -1
- package/src/modules/App/Routing/Logout/index.tsx +3 -5
- package/src/modules/App/Routing/NavMenu/NavItem/index.tsx +73 -52
- package/src/modules/App/Routing/NavMenu/NavItem/style.tsx +21 -7
- package/src/modules/App/Routing/NavMenu/index.tsx +59 -54
- package/src/modules/App/Routing/NavMenu/style.tsx +13 -11
- package/src/modules/CreatePass/index.tsx +1 -1
- package/src/modules/FileDrive/FileDragAndDrop/index.tsx +11 -8
- package/src/modules/FileDrive/FileModal/index.tsx +8 -9
- package/src/modules/FileDrive/index.tsx +1 -18
- package/src/modules/Forms/FormEditor/index.tsx +1 -1
- package/src/modules/FramePreview/HeadingsOverlay/index.tsx +22 -11
- package/src/modules/FramePreview/HeadingsOverlay/style.tsx +1 -1
- package/src/modules/MediaGallery/ImageModal/index.tsx +1 -5
- package/src/modules/MediaGallery/index.tsx +1 -3
- package/src/modules/Settings/Globals/constants.tsx +942 -106
- package/src/modules/Sites/SitesList/AllSitesHeader/index.tsx +33 -0
- package/src/modules/Sites/SitesList/AllSitesHeader/style.tsx +35 -0
- package/src/modules/Sites/SitesList/GridView/GridHeaderFilter/index.tsx +5 -5
- package/src/modules/Sites/SitesList/GridView/GridSiteItem/index.tsx +23 -119
- package/src/modules/Sites/SitesList/ListView/BulkHeader/TableHeader/index.tsx +4 -4
- package/src/modules/Sites/SitesList/ListView/BulkHeader/index.tsx +4 -3
- package/src/modules/Sites/SitesList/ListView/ListSiteItem/index.tsx +23 -120
- package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/index.tsx +4 -5
- package/src/modules/Sites/SitesList/RecentSites/index.tsx +49 -0
- package/src/modules/Sites/SitesList/RecentSites/style.tsx +92 -0
- package/src/modules/Sites/SitesList/SiteModal/index.tsx +8 -7
- package/src/modules/Sites/SitesList/WelcomeModal/DataStep/index.tsx +72 -0
- package/src/modules/Sites/SitesList/WelcomeModal/DataStep/style.tsx +59 -0
- package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/constants.tsx +78 -0
- package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/index.tsx +78 -0
- package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/style.tsx +141 -0
- package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/index.tsx +93 -0
- package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/style.tsx +77 -0
- package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/index.tsx +100 -0
- package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/style.tsx +94 -0
- package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/index.tsx +44 -0
- package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/style.tsx +31 -0
- package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/index.tsx +51 -0
- package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/style.tsx +52 -0
- package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/index.tsx +40 -0
- package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/style.tsx +53 -0
- package/src/modules/Sites/SitesList/WelcomeModal/index.tsx +215 -0
- package/src/modules/Sites/SitesList/WelcomeModal/style.tsx +12 -0
- package/src/modules/Sites/SitesList/WelcomeModal/utils.ts +26 -0
- package/src/modules/Sites/SitesList/atoms.tsx +4 -4
- package/src/modules/Sites/SitesList/hooks.tsx +149 -16
- package/src/modules/Sites/SitesList/index.tsx +127 -125
- package/src/modules/Sites/SitesList/style.tsx +1 -117
- package/src/modules/Sites/SitesList/utils.tsx +9 -2
- package/src/modules/Sites/index.tsx +19 -8
- package/src/modules/Users/Profile/index.tsx +169 -31
- package/src/modules/Users/Profile/style.tsx +81 -1
- package/src/modules/Users/Roles/RoleItem/index.tsx +2 -2
- package/src/modules/Users/UserCreate/SiteItem/index.tsx +11 -14
- package/src/modules/Users/UserForm/atoms.tsx +3 -3
- package/src/modules/Users/UserForm/index.tsx +25 -29
- package/src/modules/Users/UserForm/style.tsx +15 -2
- package/src/modules/Users/UserList/UserItem/index.tsx +4 -4
- package/src/routes/index.tsx +1 -0
- package/src/types/index.tsx +2 -0
- /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
|
|
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 {
|
|
5
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
57
|
+
dropDepthRef.current++;
|
|
58
|
+
setInDropZone(true);
|
|
50
59
|
};
|
|
51
60
|
|
|
52
61
|
const handleDragLeave = () => {
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
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
|
|
72
|
-
|
|
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
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
|
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: {
|
|
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: {
|
|
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="
|
|
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={
|
|
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
|
-
{
|
|
73
|
-
const isSelected = keywordsFilter.includes(
|
|
72
|
+
{Object.keys(keywordCounts).map((key, index) => {
|
|
73
|
+
const isSelected = keywordsFilter.includes(key);
|
|
74
74
|
return (
|
|
75
75
|
<KeywordItem
|
|
76
|
-
keyword={
|
|
77
|
-
count={keywordCounts[
|
|
76
|
+
keyword={key}
|
|
77
|
+
count={keywordCounts[key]}
|
|
78
78
|
isSelected={isSelected}
|
|
79
|
-
onClick={handleAddTag(
|
|
79
|
+
onClick={handleAddTag(key)}
|
|
80
80
|
deleteKeyword={handleDeleteKeyword}
|
|
81
|
-
key={`${
|
|
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()
|
|
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()
|
|
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
|
|
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
|
-
|
|
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
|
|
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 };
|