@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,72 @@
1
+ import { Avatar, Button, FieldsBehavior } from "@ax/components";
2
+ import * as S from "./style";
3
+ import type { IUserFormState } from "..";
4
+
5
+ const DataStep = (props: IProps) => {
6
+ const { onClickNext, onClickBack, form, setForm, croppedImageUrl } = props;
7
+ const { name, username, company, position } = form;
8
+
9
+ const handleUsernameChange = (value: string) => setForm({ ...form, username: value });
10
+ const handleJobChange = (value: string) => setForm({ ...form, position: value });
11
+ const handleCompanyChange = (value: string) => setForm({ ...form, company: value });
12
+
13
+ return (
14
+ <S.Wrapper>
15
+ <S.InfoWrapper>
16
+ <S.Title>Confirm your data</S.Title>
17
+ <S.Text>Please confirm your data to complete your profile.</S.Text>
18
+ </S.InfoWrapper>
19
+ <S.FormWrapper>
20
+ <S.FormImage>
21
+ <Avatar size={180} image={croppedImageUrl} name={name} fontSize={100} bold background />
22
+ </S.FormImage>
23
+ <S.FormFields>
24
+ <FieldsBehavior
25
+ title="Alias"
26
+ name="username"
27
+ fieldType="TextField"
28
+ placeholder="Type an alias"
29
+ mandatory
30
+ value={username}
31
+ onChange={handleUsernameChange}
32
+ prefix="@"
33
+ />
34
+ <FieldsBehavior
35
+ title="Job title"
36
+ name="position"
37
+ fieldType="TextField"
38
+ placeholder="Type a job title"
39
+ value={position}
40
+ onChange={handleJobChange}
41
+ />
42
+ <FieldsBehavior
43
+ title="Company"
44
+ name="company"
45
+ fieldType="TextField"
46
+ placeholder="Type a company"
47
+ value={company}
48
+ onChange={handleCompanyChange}
49
+ />
50
+ </S.FormFields>
51
+ </S.FormWrapper>
52
+ <S.Actions>
53
+ <Button type="button" buttonStyle="text" onClick={onClickBack}>
54
+ Back
55
+ </Button>
56
+ <Button type="button" onClick={() => onClickNext()} disabled={!username.trim().length}>
57
+ Next step
58
+ </Button>
59
+ </S.Actions>
60
+ </S.Wrapper>
61
+ );
62
+ };
63
+
64
+ interface IProps {
65
+ form: IUserFormState;
66
+ setForm: (data: IUserFormState) => void;
67
+ onClickNext: () => void;
68
+ onClickBack: () => void;
69
+ croppedImageUrl: string | null;
70
+ }
71
+
72
+ export default DataStep;
@@ -0,0 +1,59 @@
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
+ padding: ${(p) => `32px ${p.theme.spacing.m} ${p.theme.spacing.m} ${p.theme.spacing.m}`};
9
+ `;
10
+
11
+ const InfoWrapper = styled.div`
12
+ width: 450px;
13
+ margin-left: 85px;
14
+ margin-bottom: ${(p) => p.theme.spacing.m};
15
+ `;
16
+
17
+ const Title = styled.div`
18
+ ${(p) => p.theme.textStyle.headingM};
19
+ margin-bottom: ${(p) => p.theme.spacing.xs};
20
+ `;
21
+
22
+ const Text = styled.div`
23
+ ${(p) => p.theme.textStyle.uiS};
24
+ font-weight: 400;
25
+ `;
26
+
27
+ const Actions = styled.div`
28
+ display: flex;
29
+ gap: ${(p) => p.theme.spacing.s};
30
+ justify-content: flex-end;
31
+ margin-top: auto;
32
+ `;
33
+
34
+ const FormWrapper = styled.div`
35
+ display: flex;
36
+ margin-left: 85px;
37
+ margin-top: ${(p) => p.theme.spacing.xs};
38
+ gap: ${(p) => p.theme.spacing.l};
39
+ `;
40
+
41
+ const FormImage = styled.div`
42
+ width: 180px;
43
+ height: 180px;
44
+ border-radius: 50%;
45
+ overflow: hidden;
46
+ flex-shrink: 0;
47
+
48
+ img {
49
+ width: 100%;
50
+ height: 100%;
51
+ object-fit: cover;
52
+ }
53
+ `;
54
+
55
+ const FormFields = styled.div`
56
+ width: 344px;
57
+ `;
58
+
59
+ export { Wrapper, InfoWrapper, Title, Text, Actions, FormWrapper, FormImage, FormFields };
@@ -0,0 +1,78 @@
1
+ const LEARN_ITEMS = [
2
+ {
3
+ icon: "project",
4
+ title: "Create sites",
5
+ description:
6
+ "Create as many sites as you need, there is no limit. You can separate your sites by schools, by business areas, by campaigns...",
7
+ url: "https://docs.griddo.io/primeros-pasos/crear-site/",
8
+ roles: ["Administrator"],
9
+ },
10
+ {
11
+ icon: "page",
12
+ title: "Create pages",
13
+ description: "Pages are very flexible and are created by adding modules that you can place in any order you want.",
14
+ url: "https://docs.griddo.io/editor/crear-y-editar-paginas/",
15
+ roles: ["Administrator", "Webmaster", "Editor"],
16
+ },
17
+ {
18
+ icon: "working",
19
+ title: "Create Add-ons",
20
+ description: "Add custom code blocks to your site to add a variety of custom functions.",
21
+ url: "https://docs.griddo.io/configuracion/add-ons/",
22
+ roles: ["Administrator"],
23
+ },
24
+ {
25
+ icon: "category",
26
+ title: "Work with Categories",
27
+ description:
28
+ "Categories allow you to organise the content of a web page, either by subject matter or by their place in the site hierarchy.",
29
+ url: "https://docs.griddo.io/content-types/categorias/",
30
+ roles: ["Webmaster"],
31
+ },
32
+ {
33
+ icon: "ia",
34
+ title: "Work with AI",
35
+ description: "We have integrated artificial intelligence services to help users be more efficient and productive.",
36
+ url: "https://docs.griddo.io/editor/trabajar-con-ia/",
37
+ roles: ["Webmaster", "Editor"],
38
+ },
39
+ {
40
+ icon: "image",
41
+ title: "Add and manage Images",
42
+ description: "Upload images to the gallery from your computer to use them on any page of your site.",
43
+ url: "https://docs.griddo.io/galeria/media-gallery/",
44
+ roles: ["Editor"],
45
+ },
46
+ {
47
+ icon: "edit",
48
+ title: "Manage SEO",
49
+ description:
50
+ "Easy creation and editing of OpenGraph tags and structured data to adequately define any type of content that we have on any site.",
51
+ url: "https://docs.griddo.io/editor/crear-y-editar-paginas/#configurar-el-seo-de-la-pagina",
52
+ roles: ["SEO Validator"],
53
+ },
54
+ {
55
+ icon: "page",
56
+ title: "Edit pages",
57
+ description: "Pages are very flexible and are created by adding modules that you can place in any order you want.",
58
+ url: "https://docs.griddo.io/editor/crear-y-editar-paginas/",
59
+ roles: ["SEO Validator"],
60
+ },
61
+ {
62
+ icon: "search",
63
+ title: "Preview your SEO",
64
+ description:
65
+ "Review your page's heading structure and keyword usage from the editor, so your content is ready for search engines before you hit publish.",
66
+ url: "https://docs.griddo.io/editor/seo-previews/",
67
+ roles: ["SEO Validator"],
68
+ },
69
+ {
70
+ icon: "page",
71
+ title: "View pages",
72
+ description: "Pages are very flexible and are created by adding modules that you can place in any order you want.",
73
+ url: "https://docs.griddo.io/editor/crear-y-editar-paginas/",
74
+ roles: ["Viewer"],
75
+ },
76
+ ];
77
+
78
+ export { LEARN_ITEMS };
@@ -0,0 +1,78 @@
1
+ import { Button, Icon, UserRolesAndSites } from "@ax/components";
2
+ import type { IRole, ISite, ISiteRoles } from "@ax/types";
3
+
4
+ import { getMostFrequentRole } from "../utils";
5
+ import { LEARN_ITEMS } from "./constants";
6
+
7
+ import * as S from "./style";
8
+
9
+ const FinalStep = (props: IFinalStepProps) => {
10
+ const { isSuperAdmin, userRoles, roles, sites, onClose, onViewProfile } = props;
11
+
12
+ const showRows =
13
+ isSuperAdmin || (!!userRoles.find((ur) => ur.siteId === "all") && !userRoles.find((ur) => ur.siteId === "global"));
14
+
15
+ const mostFrequentRoleId = getMostFrequentRole(userRoles);
16
+ const mostFrequentRole = mostFrequentRoleId ? roles.find((r) => r.id === mostFrequentRoleId) : null;
17
+ const mostFrequentRoleName = isSuperAdmin ? "Administrator" : mostFrequentRole?.name;
18
+
19
+ let filteredLearnItems = mostFrequentRoleName
20
+ ? LEARN_ITEMS.filter((item) => item.roles.includes(mostFrequentRoleName))
21
+ : [];
22
+
23
+ if (filteredLearnItems.length === 0) {
24
+ filteredLearnItems = LEARN_ITEMS.filter((item) => item.roles.includes("Editor"));
25
+ }
26
+
27
+ return (
28
+ <S.Wrapper isRow={!showRows}>
29
+ <S.RolesWrapper>
30
+ <S.SectionTitle>You have these permissions</S.SectionTitle>
31
+ <S.Text>
32
+ Build and publish all your websites, landing pages and events easily
33
+ <br />
34
+ from a single platform.
35
+ </S.Text>
36
+ <UserRolesAndSites isSuperAdmin={isSuperAdmin} userRoles={userRoles} sites={sites} roles={roles} />
37
+ </S.RolesWrapper>
38
+ <S.LearnWrapper>
39
+ <S.LearnTitle>Learn how to use</S.LearnTitle>
40
+ <S.ItemsWrapper>
41
+ {filteredLearnItems.map(({ icon, title, description, url }) => (
42
+ <S.LearnItem key={title}>
43
+ <S.LearnHeader>
44
+ <S.IconWrapper>
45
+ <Icon name={icon} />
46
+ </S.IconWrapper>
47
+ <S.ItemTitle>{title}</S.ItemTitle>
48
+ </S.LearnHeader>
49
+ <S.Description>{description}</S.Description>
50
+ <Button type="button" buttonStyle="minimal" icon="openOutside" onClick={() => window.open(url, "_blank")}>
51
+ view
52
+ </Button>
53
+ </S.LearnItem>
54
+ ))}
55
+ </S.ItemsWrapper>
56
+ <S.Actions>
57
+ <Button type="button" buttonStyle="text" onClick={onViewProfile}>
58
+ View profile
59
+ </Button>
60
+ <Button type="button" onClick={onClose}>
61
+ Start Using Griddo
62
+ </Button>
63
+ </S.Actions>
64
+ </S.LearnWrapper>
65
+ </S.Wrapper>
66
+ );
67
+ };
68
+
69
+ interface IFinalStepProps {
70
+ isSuperAdmin: boolean;
71
+ userRoles: ISiteRoles[];
72
+ roles: IRole[];
73
+ sites: ISite[];
74
+ onClose: () => void;
75
+ onViewProfile: () => void;
76
+ }
77
+
78
+ export default FinalStep;
@@ -0,0 +1,141 @@
1
+ import styled from "styled-components";
2
+
3
+ const LearnWrapper = styled.div`
4
+ display: flex;
5
+ background-color: ${(p) => p.theme.color.uiBackground02};
6
+ flex-direction: column;
7
+ flex-shrink: 0;
8
+ `;
9
+
10
+ const ItemsWrapper = styled.div`
11
+ display: flex;
12
+ gap: ${(p) => p.theme.spacing.xs};
13
+ width: 100%;
14
+ `;
15
+
16
+ const LearnItem = styled.div`
17
+ display: flex;
18
+ flex-direction: column;
19
+ background-color: ${(p) => p.theme.color.uiBackground02};
20
+ border-radius: ${(p) => p.theme.radii.s};
21
+ border: 1px solid ${(p) => p.theme.color.uiLine};
22
+ padding: ${(p) => p.theme.spacing.s};
23
+ `;
24
+
25
+ const Actions = styled.div`
26
+ display: flex;
27
+ padding-top: ${(p) => p.theme.spacing.s};
28
+ `;
29
+
30
+ const SitesContent = styled.div``;
31
+
32
+ const Wrapper = styled.div<{ isRow: boolean }>`
33
+ display: flex;
34
+ width: 100%;
35
+ height: 100%;
36
+ flex-direction: ${(p) => (p.isRow ? "row" : "column")};
37
+
38
+ ${LearnWrapper} {
39
+ width: ${(p) => (p.isRow ? "344px" : "100%")};
40
+ height: ${(p) => (p.isRow ? "100%" : "273px")};
41
+ padding: ${(p) => (p.isRow ? p.theme.spacing.m : `${p.theme.spacing.m} 40px 0 40px`)};
42
+ }
43
+
44
+ ${ItemsWrapper} {
45
+ flex-direction: ${(p) => (p.isRow ? "column" : "row")};
46
+ }
47
+
48
+ ${LearnItem} {
49
+ width: ${(p) => (p.isRow ? "100%" : "33%")};
50
+ }
51
+
52
+ ${Actions} {
53
+ justify-content: ${(p) => (p.isRow ? "space-between" : "flex-end")};
54
+ gap: ${(p) => (p.isRow ? 0 : p.theme.spacing.s)};
55
+ margin-top: ${(p) => (p.isRow ? "auto" : 0)};
56
+ }
57
+
58
+ ${SitesContent} {
59
+ padding-top: ${(p) => (p.isRow ? p.theme.spacing.m : 0)};
60
+ }
61
+ `;
62
+
63
+ const LearnTitle = styled.div`
64
+ ${(p) => p.theme.textStyle.headingM};
65
+ margin-bottom: ${(p) => p.theme.spacing.s};
66
+ `;
67
+
68
+ const LearnHeader = styled.div`
69
+ display: flex;
70
+ align-items: center;
71
+ margin-bottom: ${(p) => p.theme.spacing.xs};
72
+ `;
73
+
74
+ const ItemTitle = styled.div`
75
+ ${(p) => p.theme.textStyle.uiM};
76
+ font-weight: 600;
77
+ margin-left: ${(p) => p.theme.spacing.xxs};
78
+ `;
79
+
80
+ const IconWrapper = styled.div`
81
+ width: ${(p) => p.theme.spacing.m};
82
+ height: ${(p) => p.theme.spacing.m};
83
+ `;
84
+
85
+ const Description = styled.div`
86
+ ${(p) => p.theme.textStyle.uiXS};
87
+ margin-bottom: ${(p) => p.theme.spacing.xs};
88
+ flex: 1;
89
+ `;
90
+
91
+ const RolesWrapper = styled.div`
92
+ width: 100%;
93
+ height: 100%;
94
+ overflow-y: auto;
95
+ padding: ${(p) => `32px 40px ${p.theme.spacing.s} 40px`};
96
+
97
+ &::-webkit-scrollbar {
98
+ -webkit-appearance: none;
99
+ width: 4px;
100
+ height: 100%;
101
+ }
102
+
103
+ &::-webkit-scrollbar-thumb {
104
+ border-radius: 4px;
105
+ background-color: ${(p) => p.theme.color.iconNonActive};
106
+ }
107
+ `;
108
+
109
+ const SectionTitle = styled.div`
110
+ font-size: 28px;
111
+ line-height: 25px;
112
+ font-weight: 700;
113
+ margin-bottom: ${(p) => p.theme.spacing.s};
114
+ `;
115
+
116
+ const Text = styled.div`
117
+ ${(p) => p.theme.textStyle.uiL};
118
+ font-weight: 400;
119
+ p {
120
+ margin-bottom: ${(p) => p.theme.spacing.s};
121
+ }
122
+ strong {
123
+ font-weight: 600;
124
+ }
125
+ `;
126
+
127
+ export {
128
+ Wrapper,
129
+ Actions,
130
+ LearnWrapper,
131
+ RolesWrapper,
132
+ LearnTitle,
133
+ LearnItem,
134
+ LearnHeader,
135
+ IconWrapper,
136
+ Description,
137
+ ItemTitle,
138
+ ItemsWrapper,
139
+ SectionTitle,
140
+ Text,
141
+ };
@@ -0,0 +1,93 @@
1
+ import { useCallback, useState } from "react";
2
+ import type { Area } from "react-easy-crop";
3
+ import Cropper from "react-easy-crop";
4
+
5
+ import { Button } from "@ax/components";
6
+
7
+ import * as S from "./style";
8
+
9
+ const MIN_ZOOM = 1;
10
+ const MAX_ZOOM = 3;
11
+ const ZOOM_STEP = 0.1;
12
+
13
+ const CropView = (props: IProps) => {
14
+ const { imageSrc, onConfirm, onUploadNew } = props;
15
+
16
+ const [crop, setCrop] = useState({ x: 0, y: 0 });
17
+ const [zoom, setZoom] = useState(1);
18
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
19
+ const [objectFit, setObjectFit] = useState<"vertical-cover" | "horizontal-cover">("vertical-cover");
20
+
21
+ const onCropComplete = useCallback((_: Area, pixels: Area) => {
22
+ setCroppedAreaPixels(pixels);
23
+ }, []);
24
+
25
+ const onMediaLoaded = useCallback((mediaSize: { width: number; height: number }) => {
26
+ const isPortrait = mediaSize.height > mediaSize.width;
27
+ setObjectFit(isPortrait ? "vertical-cover" : "horizontal-cover");
28
+ }, []);
29
+
30
+ const handleConfirm = () => {
31
+ if (croppedAreaPixels) onConfirm(croppedAreaPixels);
32
+ };
33
+
34
+ const handleZoomIn = () => setZoom((z) => Math.min(z + ZOOM_STEP, MAX_ZOOM));
35
+ const handleZoomOut = () => setZoom((z) => Math.max(z - ZOOM_STEP, MIN_ZOOM));
36
+
37
+ return (
38
+ <S.Wrapper>
39
+ <S.CropArea>
40
+ <S.CropContainer>
41
+ <Cropper
42
+ image={imageSrc}
43
+ crop={crop}
44
+ zoom={zoom}
45
+ aspect={1}
46
+ cropShape="round"
47
+ showGrid={false}
48
+ onCropChange={setCrop}
49
+ onZoomChange={setZoom}
50
+ onCropComplete={onCropComplete}
51
+ onMediaLoaded={onMediaLoaded}
52
+ objectFit={objectFit}
53
+ />
54
+ </S.CropContainer>
55
+ <S.ZoomControls>
56
+ <S.ZoomButton type="button" onClick={handleZoomIn} aria-label="Zoom in">
57
+ +
58
+ </S.ZoomButton>
59
+ <S.ZoomSlider
60
+ type="range"
61
+ min={MIN_ZOOM}
62
+ max={MAX_ZOOM}
63
+ step={ZOOM_STEP}
64
+ value={zoom}
65
+ onChange={(e) => setZoom(Number(e.target.value))}
66
+ aria-label="Zoom"
67
+ />
68
+ <S.ZoomButton type="button" onClick={handleZoomOut} aria-label="Zoom out">
69
+ {"\u2212"}
70
+ </S.ZoomButton>
71
+ </S.ZoomControls>
72
+ </S.CropArea>
73
+ <S.ButtonWrapper>
74
+ <Button type="button" buttonStyle="minimal" onClick={onUploadNew} icon="refresh">
75
+ Upload new image
76
+ </Button>
77
+ </S.ButtonWrapper>
78
+ <S.Actions>
79
+ <Button type="button" buttonStyle="solid" onClick={handleConfirm}>
80
+ Next step
81
+ </Button>
82
+ </S.Actions>
83
+ </S.Wrapper>
84
+ );
85
+ };
86
+
87
+ interface IProps {
88
+ imageSrc: string;
89
+ onConfirm: (croppedAreaPixels: Area) => void;
90
+ onUploadNew: () => void;
91
+ }
92
+
93
+ export default CropView;
@@ -0,0 +1,77 @@
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
+ gap: ${(p) => p.theme.spacing.m};
16
+ margin-top: ${(p) => p.theme.spacing.s};
17
+ margin-left: ${(p) => p.theme.spacing.l};
18
+ `;
19
+
20
+ const CropContainer = styled.div`
21
+ position: relative;
22
+ width: 276px;
23
+ height: 276px;
24
+ border-radius: 50%;
25
+ overflow: hidden;
26
+ `;
27
+
28
+ const ZoomControls = styled.div`
29
+ display: flex;
30
+ flex-direction: column;
31
+ align-items: center;
32
+ gap: ${(p) => p.theme.spacing.xs};
33
+ `;
34
+
35
+ const ZoomButton = styled.button`
36
+ background: none;
37
+ border: none;
38
+ cursor: pointer;
39
+ color: ${(p) => p.theme.color.interactive01};
40
+ font-size: 24px;
41
+ line-height: 1;
42
+ padding: 0;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: 24px;
47
+ height: 24px;
48
+
49
+ &:hover {
50
+ opacity: 0.7;
51
+ }
52
+ `;
53
+
54
+ const ZoomSlider = styled.input`
55
+ appearance: slider-vertical;
56
+ writing-mode: vertical-lr;
57
+ direction: rtl;
58
+ width: 4px;
59
+ height: 216px;
60
+ cursor: pointer;
61
+ accent-color: ${(p) => p.theme.color.interactive01};
62
+ `;
63
+
64
+ const Actions = styled.div`
65
+ display: flex;
66
+ width: 100%;
67
+ justify-content: flex-end;
68
+ `;
69
+
70
+ const ButtonWrapper = styled.div`
71
+ display: flex;
72
+ width: 100%;
73
+ justify-content: center;
74
+ padding-top: ${(p) => p.theme.spacing.xs};
75
+ `;
76
+
77
+ export { Actions, Wrapper, CropArea, CropContainer, ZoomControls, ZoomButton, ZoomSlider, ButtonWrapper };
@@ -0,0 +1,100 @@
1
+ import { useRef, useState } from "react";
2
+
3
+ import { Button, DragAndDrop, Icon } from "@ax/components";
4
+
5
+ import * as S from "./style";
6
+
7
+ const VALID_FORMATS = ["jpg", "jpeg", "png", "webp"];
8
+ const VALID_EXTENSIONS = VALID_FORMATS.map((f) => `.${f}`).join(",");
9
+
10
+ const UploadView = (props: IProps) => {
11
+ const { onImageLoaded, onSkip } = props;
12
+
13
+ const inputRef = useRef<HTMLInputElement | null>(null);
14
+ const [inDropZone, setInDropZone] = useState(false);
15
+ const [dropDepth, setDropDepth] = useState(0);
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const isValidFormat = (fileName: string) => {
19
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
20
+ return VALID_FORMATS.includes(ext);
21
+ };
22
+
23
+ const readFile = (file: File) => {
24
+ if (!isValidFormat(file.name)) {
25
+ setError(`Invalid format. Allowed: ${VALID_FORMATS.join(", ")}`);
26
+ return;
27
+ }
28
+ setError(null);
29
+ const reader = new FileReader();
30
+ reader.onload = (e) => {
31
+ const result = e.target?.result;
32
+ if (typeof result === "string") onImageLoaded(result);
33
+ };
34
+ reader.readAsDataURL(file);
35
+ };
36
+
37
+ const handleDragEnter = () => setDropDepth((d) => d + 1);
38
+
39
+ const handleDragLeave = () => {
40
+ setDropDepth((d) => d - 1);
41
+ if (dropDepth > 1) return;
42
+ setInDropZone(false);
43
+ };
44
+
45
+ const handleDragOver = () => setInDropZone(true);
46
+
47
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
48
+ const file = e.dataTransfer.files[0];
49
+ if (file) readFile(file);
50
+ setInDropZone(false);
51
+ setDropDepth(0);
52
+ };
53
+
54
+ const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
55
+ const file = e.currentTarget.files?.[0];
56
+ if (file) readFile(file);
57
+ };
58
+
59
+ const handleClick = () => inputRef.current?.click();
60
+
61
+ return (
62
+ <S.Wrapper>
63
+ <S.DragAndDropWrapper>
64
+ <DragAndDrop
65
+ onDrop={handleDrop}
66
+ onDragOver={handleDragOver}
67
+ onDragEnter={handleDragEnter}
68
+ onDragLeave={handleDragLeave}
69
+ >
70
+ <S.DropZone inDropZone={inDropZone}>
71
+ <S.IconWrapper>
72
+ <Icon name="image" size="48" />
73
+ </S.IconWrapper>
74
+ <S.DragTitle>{inDropZone ? "Drop your image" : "Drag your image here"}</S.DragTitle>
75
+ {!inDropZone && <S.DragSubtitle>or</S.DragSubtitle>}
76
+ <S.FilesInput ref={inputRef} type="file" accept={VALID_EXTENSIONS} onChange={handleFileInput} />
77
+ {!inDropZone && (
78
+ <S.SelectButton type="button" buttonStyle="solid" onClick={handleClick}>
79
+ Select image
80
+ </S.SelectButton>
81
+ )}
82
+ </S.DropZone>
83
+ </DragAndDrop>
84
+ {error && <S.DragError data-testid="upload-view-error">{error}</S.DragError>}
85
+ </S.DragAndDropWrapper>
86
+ <S.Actions>
87
+ <Button type="button" buttonStyle="text" onClick={onSkip}>
88
+ Skip profile picture
89
+ </Button>
90
+ </S.Actions>
91
+ </S.Wrapper>
92
+ );
93
+ };
94
+
95
+ interface IProps {
96
+ onImageLoaded: (imageSrc: string) => void;
97
+ onSkip: () => void;
98
+ }
99
+
100
+ export default UploadView;