@griddo/ax 11.11.7 → 11.11.8-rc.0
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/componentsMock.js +7 -5
- package/package.json +2 -2
- package/src/__tests__/components/Browser/Browser.test.tsx +438 -87
- package/src/__tests__/components/Browser/Browser.utils.test.ts +55 -0
- package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +1 -3
- package/src/__tests__/components/Fields/Button/Button.test.tsx +29 -27
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +158 -0
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +90 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +178 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +150 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +91 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +122 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +15 -0
- package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +101 -0
- package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
- package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +318 -0
- package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +242 -0
- package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +185 -0
- package/src/components/Browser/index.tsx +301 -134
- package/src/components/Browser/style.tsx +75 -6
- package/src/components/Browser/utils.tsx +13 -0
- package/src/components/Button/index.tsx +2 -1
- package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
- package/src/components/Fields/AsyncSelect/style.tsx +13 -0
- package/src/components/Fields/FieldGroup/index.tsx +5 -2
- package/src/components/Fields/FieldGroup/style.tsx +32 -7
- package/src/components/Fields/HeadingField/index.tsx +2 -2
- package/src/components/Fields/HiddenField/style.tsx +1 -1
- package/src/components/Fields/NumberField/index.tsx +15 -16
- package/src/components/Fields/NumberField/style.tsx +2 -0
- package/src/components/Fields/ReferenceField/index.tsx +1 -1
- package/src/components/Fields/SEOPreview/index.tsx +36 -0
- package/src/components/Fields/SEOPreview/style.tsx +24 -0
- package/src/components/Fields/Select/index.tsx +5 -1
- package/src/components/Fields/Select/style.tsx +56 -0
- package/src/components/Fields/SummaryButton/index.tsx +18 -9
- package/src/components/Fields/SummaryButton/style.tsx +1 -2
- package/src/components/Fields/TagsField/index.tsx +8 -9
- package/src/components/Fields/UrlField/index.tsx +26 -27
- package/src/components/Fields/index.tsx +2 -0
- package/src/components/FloatingNote/index.tsx +35 -0
- package/src/components/FloatingNote/style.tsx +26 -0
- package/src/components/FloatingPanel/index.tsx +5 -2
- package/src/components/FloatingPanel/style.tsx +2 -1
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
- package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
- package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
- package/src/components/HeadingsPreviewModal/index.tsx +146 -0
- package/src/components/HeadingsPreviewModal/style.tsx +82 -0
- package/src/components/HeadingsPreviewModal/utils.tsx +257 -0
- package/src/components/IconAction/index.tsx +1 -1
- package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
- package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
- package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
- package/src/components/KeywordsPreviewModal/index.tsx +99 -0
- package/src/components/KeywordsPreviewModal/style.tsx +87 -0
- package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
- package/src/components/MainWrapper/AppBar/index.tsx +8 -1
- package/src/components/MainWrapper/index.tsx +7 -1
- package/src/components/Notification/index.tsx +2 -2
- package/src/components/PageFinder/index.tsx +1 -1
- package/src/components/ResizePanel/index.tsx +4 -3
- package/src/components/ResizePanel/style.tsx +1 -1
- package/src/components/SearchField/style.tsx +2 -2
- package/src/components/SideModal/index.tsx +2 -1
- package/src/components/Tabs/index.tsx +13 -4
- package/src/components/Tabs/style.tsx +7 -8
- package/src/components/Toast/index.tsx +4 -2
- package/src/components/Tooltip/index.tsx +4 -3
- package/src/components/index.tsx +8 -0
- package/src/forms/fields.tsx +70 -68
- package/src/hooks/forms.tsx +22 -1
- package/src/hooks/index.tsx +13 -3
- package/src/hooks/modals.tsx +103 -15
- package/src/hooks/users.tsx +25 -8
- package/src/modules/Forms/atoms.tsx +2 -2
- package/src/modules/FramePreview/HeadingsOverlay/index.tsx +113 -0
- package/src/modules/FramePreview/HeadingsOverlay/style.tsx +24 -0
- package/src/modules/FramePreview/index.tsx +55 -16
- package/src/modules/FramePreview/style.tsx +34 -2
- package/src/modules/FramePreview/utils.tsx +140 -0
- package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
- package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
- package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
- package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
- package/src/modules/GlobalEditor/index.tsx +119 -57
- package/src/modules/PageEditor/Editor/index.tsx +33 -2
- package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
- package/src/modules/PageEditor/Preview/index.tsx +0 -2
- package/src/modules/PageEditor/Preview/style.tsx +1 -1
- package/src/modules/PageEditor/atoms.tsx +1 -1
- package/src/modules/PageEditor/index.tsx +130 -66
- package/src/modules/PublicPreview/index.tsx +5 -2
- package/src/schemas/pages/GlobalPage.ts +87 -70
- package/src/schemas/pages/Page.ts +87 -70
- package/src/types/index.tsx +12 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { IconAction, TextField, FloatingPanel, FieldsBehavior, PageFinder } from "@ax/components";
|
|
4
4
|
import { useModal } from "@ax/hooks";
|
|
5
5
|
import { isReqOk } from "@ax/helpers";
|
|
6
|
-
import { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
|
|
6
|
+
import type { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
|
|
7
7
|
import { pages as pagesApi } from "@ax/api";
|
|
8
8
|
import { findAnchorsFromPage, findAnchorsFromTab, findTabsFromPage } from "./utils";
|
|
9
9
|
|
|
@@ -29,7 +29,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
29
29
|
const [internalPageName, setInternalPageName] = useState(null);
|
|
30
30
|
const [pageData, setPageData] = useState<IPage | null>(null);
|
|
31
31
|
|
|
32
|
-
const pageID = value
|
|
32
|
+
const pageID = value?.linkTo ? value.linkTo : null;
|
|
33
33
|
|
|
34
34
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
35
35
|
useEffect(() => {
|
|
@@ -67,13 +67,13 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
67
67
|
|
|
68
68
|
const handleOnClick = () => {
|
|
69
69
|
toggleModal();
|
|
70
|
-
handlePanel
|
|
70
|
+
handlePanel?.(isOpen);
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
const handleReset = () => {
|
|
74
74
|
onChange(null);
|
|
75
75
|
setInternalPageName(null);
|
|
76
|
-
resetValidation
|
|
76
|
+
resetValidation?.();
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
const handleSetPage = (page: IPage | IPage[]) => {
|
|
@@ -85,10 +85,10 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
85
85
|
linkTo: selectedPage.id,
|
|
86
86
|
linkToURL: selectedPage.fullUrl,
|
|
87
87
|
title: selectedPage.title,
|
|
88
|
-
noFollow: !selectedPage.follow
|
|
88
|
+
noFollow: !selectedPage.follow,
|
|
89
89
|
});
|
|
90
|
-
handleValidation
|
|
91
|
-
handlePanel
|
|
90
|
+
handleValidation?.(selectedPage.id.toString(), validators);
|
|
91
|
+
handlePanel?.(isOpen);
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const handleChange = (newValue: string) => onChange({ ...value, href: newValue });
|
|
@@ -113,7 +113,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
const validator = { format: "fullURL" };
|
|
116
|
-
const defensiveHref = value
|
|
116
|
+
const defensiveHref = value?.href ? value.href : "";
|
|
117
117
|
|
|
118
118
|
let field = (
|
|
119
119
|
<TextField
|
|
@@ -140,7 +140,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
const titleValue = value
|
|
143
|
+
const titleValue = value?.title ? value.title : internalPageName ? internalPageName : "";
|
|
144
144
|
const noFollowValue =
|
|
145
145
|
value && value.noFollow !== undefined
|
|
146
146
|
? value.noFollow
|
|
@@ -160,7 +160,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
160
160
|
type: "UniqueCheck" as Field,
|
|
161
161
|
name: "newTab",
|
|
162
162
|
options: [{ title: "Open in new tab" }],
|
|
163
|
-
value: value
|
|
163
|
+
value: value?.newTab ? value.newTab : null,
|
|
164
164
|
onChange: handleNewTabChange,
|
|
165
165
|
},
|
|
166
166
|
{
|
|
@@ -175,7 +175,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
175
175
|
return (
|
|
176
176
|
<S.UrlFieldWrapper data-testid="url-field-wrapper">
|
|
177
177
|
{field}
|
|
178
|
-
{value
|
|
178
|
+
{value?.linkTo && isTabsVisible && (
|
|
179
179
|
<S.AnchorWrapper>
|
|
180
180
|
<FieldsBehavior
|
|
181
181
|
title="Tab"
|
|
@@ -188,7 +188,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
188
188
|
/>
|
|
189
189
|
</S.AnchorWrapper>
|
|
190
190
|
)}
|
|
191
|
-
{value
|
|
191
|
+
{value?.linkTo && isVisible && (
|
|
192
192
|
<S.AnchorWrapper>
|
|
193
193
|
<FieldsBehavior
|
|
194
194
|
title="Anchor"
|
|
@@ -203,20 +203,19 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
203
203
|
)}
|
|
204
204
|
{showAdvanced && (
|
|
205
205
|
<S.AdvancedWrapper>
|
|
206
|
-
{advancedFields
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
})}
|
|
206
|
+
{advancedFields?.map((adField) => {
|
|
207
|
+
return (
|
|
208
|
+
<FieldsBehavior
|
|
209
|
+
key={adField.name}
|
|
210
|
+
options={adField.options}
|
|
211
|
+
name={adField.name}
|
|
212
|
+
title={adField.title}
|
|
213
|
+
fieldType={adField.type}
|
|
214
|
+
value={adField.value}
|
|
215
|
+
onChange={adField.onChange}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
220
219
|
</S.AdvancedWrapper>
|
|
221
220
|
)}
|
|
222
221
|
<FloatingPanel
|
|
@@ -17,6 +17,7 @@ import FormCategorySelect from "./FormCategorySelect";
|
|
|
17
17
|
import FormContainer from "./FormContainer";
|
|
18
18
|
import FormFieldArray from "./FormFieldArray";
|
|
19
19
|
import HeadingField from "./HeadingField";
|
|
20
|
+
import SEOPreview from "./SEOPreview";
|
|
20
21
|
import HiddenField from "./HiddenField";
|
|
21
22
|
import ImageField from "./ImageField";
|
|
22
23
|
import LinkField from "./LinkField";
|
|
@@ -64,6 +65,7 @@ export {
|
|
|
64
65
|
FormContainer,
|
|
65
66
|
FormFieldArray,
|
|
66
67
|
HeadingField,
|
|
68
|
+
SEOPreview,
|
|
67
69
|
HiddenField,
|
|
68
70
|
ImageField,
|
|
69
71
|
LinkField,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Button, Icon } from "@ax/components";
|
|
2
|
+
|
|
3
|
+
import * as S from "./style";
|
|
4
|
+
|
|
5
|
+
const FloatingNote = (props: IFloatingNoteProps): JSX.Element => {
|
|
6
|
+
const { message, icon, className, btnText, onClick } = props;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<S.Wrapper data-testid="floating-note-wrapper" className={className}>
|
|
10
|
+
{icon && (
|
|
11
|
+
<S.IconWrapper>
|
|
12
|
+
<Icon name={icon} size="16" />
|
|
13
|
+
</S.IconWrapper>
|
|
14
|
+
)}
|
|
15
|
+
<S.Text data-testid="floating-note-message">{message}</S.Text>
|
|
16
|
+
{btnText && (
|
|
17
|
+
<S.ActionWrapper>
|
|
18
|
+
<Button type="button" buttonStyle="minimal" onClick={onClick}>
|
|
19
|
+
{btnText}
|
|
20
|
+
</Button>
|
|
21
|
+
</S.ActionWrapper>
|
|
22
|
+
)}
|
|
23
|
+
</S.Wrapper>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface IFloatingNoteProps {
|
|
28
|
+
message: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
btnText?: string;
|
|
32
|
+
onClick?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default FloatingNote;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const Wrapper = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
background-color: ${(p) => p.theme.color.uiBackground03};
|
|
6
|
+
padding: ${(p) => p.theme.spacing.s};
|
|
7
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
8
|
+
z-index: 98;
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
const Text = styled.div`
|
|
12
|
+
${(p) => p.theme.textStyle.uiS};
|
|
13
|
+
color: ${(p) => p.theme.color.textMediumEmphasis};
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const IconWrapper = styled.div`
|
|
17
|
+
width: ${(p) => p.theme.spacing.s};
|
|
18
|
+
height: ${(p) => p.theme.spacing.s};
|
|
19
|
+
margin-right: ${(p) => p.theme.spacing.xs};
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const ActionWrapper = styled.div`
|
|
23
|
+
margin-left: auto;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export { Wrapper, Text, IconWrapper, ActionWrapper };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo, useRef } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
|
|
4
4
|
import { useHandleClickOutside } from "@ax/hooks";
|
|
@@ -16,12 +16,13 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
|
|
|
16
16
|
handlePanel,
|
|
17
17
|
secondary,
|
|
18
18
|
closeOnOutsideClick = true,
|
|
19
|
+
width,
|
|
19
20
|
} = props;
|
|
20
21
|
|
|
21
22
|
const node = useRef<HTMLElement>(null);
|
|
22
23
|
|
|
23
24
|
const handleClickOutside = (e: any) => {
|
|
24
|
-
if (
|
|
25
|
+
if (node.current?.contains(e.target) || isOpenedSecond || !closeOnOutsideClick) {
|
|
25
26
|
return;
|
|
26
27
|
}
|
|
27
28
|
toggleModal();
|
|
@@ -46,6 +47,7 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
|
|
|
46
47
|
isOpen={isOpen}
|
|
47
48
|
isOpenedSecond={isOpenedSecond}
|
|
48
49
|
secondary={secondary}
|
|
50
|
+
width={width}
|
|
49
51
|
>
|
|
50
52
|
<S.Header>
|
|
51
53
|
<S.Title>{title}</S.Title>
|
|
@@ -70,6 +72,7 @@ export interface IFloatingPanelProps {
|
|
|
70
72
|
handlePanel?: (value: boolean) => void;
|
|
71
73
|
secondary?: boolean;
|
|
72
74
|
closeOnOutsideClick?: boolean;
|
|
75
|
+
width?: number;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export default memo(FloatingPanel);
|
|
@@ -17,12 +17,13 @@ export const Wrapper = styled.section<{
|
|
|
17
17
|
isOpen: boolean;
|
|
18
18
|
isOpenedSecond: boolean | undefined;
|
|
19
19
|
secondary?: boolean;
|
|
20
|
+
width?: number;
|
|
20
21
|
}>`
|
|
21
22
|
position: fixed;
|
|
22
23
|
right: 0;
|
|
23
24
|
top: 0;
|
|
24
25
|
z-index: 1200;
|
|
25
|
-
width:
|
|
26
|
+
width: ${(p) => (p.width ? `${p.width}px` : `calc(${p.theme.spacing.xl} * 6)`)};
|
|
26
27
|
height: 100vh;
|
|
27
28
|
background: ${(p) => p.theme.colors.uiBackground01};
|
|
28
29
|
box-shadow: ${(p) => (p.secondary || !p.isOpen ? `none` : p.theme.shadow.rightPanel)};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Icon, Tooltip } from "@ax/components";
|
|
4
|
+
|
|
5
|
+
import type { IHeadingError } from "../../utils";
|
|
6
|
+
|
|
7
|
+
import * as S from "./style";
|
|
8
|
+
|
|
9
|
+
const ErrorsItem = (props: IErrorsItemProps) => {
|
|
10
|
+
const { error, onSelectHeading } = props;
|
|
11
|
+
const { message, description, headingIds } = error;
|
|
12
|
+
|
|
13
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
14
|
+
const [isDeleted, setIsDeleted] = useState(false);
|
|
15
|
+
const [currentIndex, setCurrentIndex] = useState<number | null>(null);
|
|
16
|
+
|
|
17
|
+
const handlePrevious = () => {
|
|
18
|
+
if (!currentIndex) return;
|
|
19
|
+
const newIndex = currentIndex - 1;
|
|
20
|
+
setCurrentIndex(newIndex);
|
|
21
|
+
onSelectHeading(headingIds[newIndex])();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleNext = () => {
|
|
25
|
+
const newIndex = currentIndex === null ? 0 : currentIndex + 1;
|
|
26
|
+
if (newIndex > headingIds.length - 1) return;
|
|
27
|
+
setCurrentIndex(newIndex);
|
|
28
|
+
onSelectHeading(headingIds[newIndex])();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleOpen = () => {
|
|
32
|
+
setIsOpen(!isOpen);
|
|
33
|
+
setCurrentIndex(0);
|
|
34
|
+
onSelectHeading(headingIds[0])();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const errorText =
|
|
38
|
+
headingIds.length > 1 && currentIndex !== null
|
|
39
|
+
? `${currentIndex + 1} of ${headingIds.length} headings`
|
|
40
|
+
: `${headingIds.length} headings`;
|
|
41
|
+
|
|
42
|
+
if (isDeleted) {
|
|
43
|
+
return <></>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<S.ErrorItem>
|
|
48
|
+
<Tooltip content={description}>
|
|
49
|
+
<S.ErrorHeader>
|
|
50
|
+
<S.ErrorMessage>{message}</S.ErrorMessage>
|
|
51
|
+
<S.ErrorActions>
|
|
52
|
+
{headingIds.length > 0 && (
|
|
53
|
+
<S.IconWrapper onClick={handleOpen}>
|
|
54
|
+
<Icon name={isOpen ? "UpArrow" : "DownArrow"} size="16" />
|
|
55
|
+
</S.IconWrapper>
|
|
56
|
+
)}
|
|
57
|
+
<S.IconWrapper onClick={() => setIsDeleted(true)}>
|
|
58
|
+
<Icon name="close" size="16" />
|
|
59
|
+
</S.IconWrapper>
|
|
60
|
+
</S.ErrorActions>
|
|
61
|
+
</S.ErrorHeader>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
<S.ErrorContentWrapper isOpen={isOpen}>
|
|
64
|
+
<S.ErrorContent>
|
|
65
|
+
<S.ErrorContentActions>
|
|
66
|
+
<S.IconWrapper onClick={handlePrevious} isDisabled={!currentIndex}>
|
|
67
|
+
<Icon name="UpArrow" size="16" />
|
|
68
|
+
</S.IconWrapper>
|
|
69
|
+
<S.IconWrapper onClick={handleNext} isDisabled={currentIndex === headingIds.length - 1}>
|
|
70
|
+
<Icon name="DownArrow" size="16" />
|
|
71
|
+
</S.IconWrapper>
|
|
72
|
+
</S.ErrorContentActions>
|
|
73
|
+
<S.ErrorContentText>{errorText}</S.ErrorContentText>
|
|
74
|
+
</S.ErrorContent>
|
|
75
|
+
</S.ErrorContentWrapper>
|
|
76
|
+
</S.ErrorItem>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface IErrorsItemProps {
|
|
81
|
+
error: IHeadingError;
|
|
82
|
+
onSelectHeading: (id: number) => () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default ErrorsItem;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const ErrorItem = styled.li`
|
|
4
|
+
display: flex;
|
|
5
|
+
background-color: ${(p) => p.theme.colors.uiBackground02};
|
|
6
|
+
border: ${(p) => `1px solid ${p.theme.colors.uiLine}`};
|
|
7
|
+
padding: ${(p) => p.theme.spacing.xs};
|
|
8
|
+
margin-bottom: ${(p) => p.theme.spacing.xxs};
|
|
9
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
|
|
12
|
+
&:last-child {
|
|
13
|
+
margin-bottom: 0;
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const ErrorHeader = styled.div`
|
|
18
|
+
display: flex;
|
|
19
|
+
width: 100%;
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const ErrorMessage = styled.div`
|
|
23
|
+
${(p) => p.theme.textStyle.uiS};
|
|
24
|
+
color: ${(p) => p.theme.colors.textHighEmphasis};
|
|
25
|
+
font-weight: 400;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const ErrorActions = styled.div`
|
|
29
|
+
display: flex;
|
|
30
|
+
margin-left: auto;
|
|
31
|
+
align-items: start;
|
|
32
|
+
gap: ${(p) => p.theme.spacing.xxs};
|
|
33
|
+
padding-left: ${(p) => p.theme.spacing.xs};
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const IconWrapper = styled.button<{ isDisabled?: boolean }>`
|
|
37
|
+
width: ${(p) => p.theme.spacing.s};
|
|
38
|
+
height: ${(p) => p.theme.spacing.s};
|
|
39
|
+
svg {
|
|
40
|
+
path {
|
|
41
|
+
fill: ${(p) => (p.isDisabled ? p.theme.color.interactiveDisabled : p.theme.color.interactive01)};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const ErrorContentWrapper = styled.div<{ isOpen: boolean }>`
|
|
47
|
+
width: 100%;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
height: ${(p) => (p.isOpen ? "auto" : "0")};
|
|
50
|
+
transition: height 1s ease-in-out;
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const ErrorContent = styled.div`
|
|
54
|
+
display: flex;
|
|
55
|
+
width: 100%;
|
|
56
|
+
margin-top: ${(p) => p.theme.spacing.xxs};
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const ErrorContentActions = styled.div`
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: ${(p) => p.theme.spacing.xxs};
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const ErrorContentText = styled.div`
|
|
65
|
+
${(p) => p.theme.textStyle.uiXS};
|
|
66
|
+
color: ${(p) => p.theme.colors.textMediumEmphasis};
|
|
67
|
+
margin-left: ${(p) => p.theme.spacing.s};
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
ErrorItem,
|
|
72
|
+
ErrorHeader,
|
|
73
|
+
ErrorMessage,
|
|
74
|
+
ErrorActions,
|
|
75
|
+
IconWrapper,
|
|
76
|
+
ErrorContentWrapper,
|
|
77
|
+
ErrorContent,
|
|
78
|
+
ErrorContentActions,
|
|
79
|
+
ErrorContentText,
|
|
80
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Icon } from "@ax/components";
|
|
4
|
+
|
|
5
|
+
import type { IHeadingError } from "../utils";
|
|
6
|
+
import ErrorItem from "./ErrorItem";
|
|
7
|
+
|
|
8
|
+
import * as S from "./style";
|
|
9
|
+
|
|
10
|
+
const ErrorsBanner = (props: IErrorsBannerProps) => {
|
|
11
|
+
const { errors, onSelectHeading, isOpen, setIsOpen, resetKey } = props;
|
|
12
|
+
|
|
13
|
+
const [isDeleted, setIsDeleted] = useState(false);
|
|
14
|
+
|
|
15
|
+
if (isDeleted) {
|
|
16
|
+
return <></>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<S.ErrorsWrapper>
|
|
21
|
+
<S.ErrorsHeader>
|
|
22
|
+
<S.WarningWrapper>
|
|
23
|
+
<Icon name="warning" size="16" />
|
|
24
|
+
</S.WarningWrapper>
|
|
25
|
+
<S.HeaderText>SEO Alerts</S.HeaderText>
|
|
26
|
+
<S.HeaderActions>
|
|
27
|
+
<S.ToggleWrapper onClick={() => setIsOpen(!isOpen)}>
|
|
28
|
+
<Icon name={isOpen ? "UpArrow" : "DownArrow"} size="24" />
|
|
29
|
+
</S.ToggleWrapper>
|
|
30
|
+
<S.IconWrapper onClick={() => setIsDeleted(true)}>
|
|
31
|
+
<Icon name="close" size="16" />
|
|
32
|
+
</S.IconWrapper>
|
|
33
|
+
</S.HeaderActions>
|
|
34
|
+
</S.ErrorsHeader>
|
|
35
|
+
<S.ErrorsContent isOpen={isOpen}>
|
|
36
|
+
<S.Description>
|
|
37
|
+
Review <strong>suggestions and warnings</strong> to enhance your page's search engine optimization.
|
|
38
|
+
</S.Description>
|
|
39
|
+
<S.ErrorListWrapper>
|
|
40
|
+
{errors.map((error) => (
|
|
41
|
+
<ErrorItem key={`${error.message}-${resetKey}`} error={error} onSelectHeading={onSelectHeading} />
|
|
42
|
+
))}
|
|
43
|
+
</S.ErrorListWrapper>
|
|
44
|
+
</S.ErrorsContent>
|
|
45
|
+
</S.ErrorsWrapper>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
interface IErrorsBannerProps {
|
|
50
|
+
errors: IHeadingError[];
|
|
51
|
+
onSelectHeading: (id: number) => () => void;
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
setIsOpen: (value: boolean) => void;
|
|
54
|
+
resetKey: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default ErrorsBanner;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const ErrorsWrapper = styled.div`
|
|
4
|
+
position: sticky;
|
|
5
|
+
top: 0;
|
|
6
|
+
background-color: ${(p) => p.theme.colors.uiBackground03};
|
|
7
|
+
width: 100%;
|
|
8
|
+
padding: ${(p) => p.theme.spacing.s};
|
|
9
|
+
margin-bottom: ${(p) => p.theme.spacing.xs};
|
|
10
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
11
|
+
z-index: 2;
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const ErrorsHeader = styled.div`
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const WarningWrapper = styled.div`
|
|
20
|
+
width: ${(p) => p.theme.spacing.s};
|
|
21
|
+
height: ${(p) => p.theme.spacing.s};
|
|
22
|
+
svg {
|
|
23
|
+
path {
|
|
24
|
+
fill: ${(p) => p.theme.color.interactive02};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const HeaderText = styled.div`
|
|
30
|
+
${(p) => p.theme.textStyle.uiM};
|
|
31
|
+
color: ${(p) => p.theme.colors.textHighEmphasis};
|
|
32
|
+
margin-left: ${(p) => p.theme.spacing.xxs};
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const HeaderActions = styled.div`
|
|
37
|
+
display: flex;
|
|
38
|
+
margin-left: auto;
|
|
39
|
+
gap: ${(p) => p.theme.spacing.xxs};
|
|
40
|
+
align-items: center;
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const ToggleWrapper = styled.button`
|
|
44
|
+
width: ${(p) => p.theme.spacing.m};
|
|
45
|
+
height: ${(p) => p.theme.spacing.m};
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const IconWrapper = styled.button`
|
|
49
|
+
width: ${(p) => p.theme.spacing.s};
|
|
50
|
+
height: ${(p) => p.theme.spacing.s};
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const ErrorsContent = styled.div<{ isOpen: boolean }>`
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
height: ${(p) => (p.isOpen ? "auto" : "0")};
|
|
58
|
+
transition: height 1s ease-in-out;
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const Description = styled.div`
|
|
62
|
+
${(p) => p.theme.textStyle.uiXS};
|
|
63
|
+
color: ${(p) => p.theme.colors.textMediumEmphasis};
|
|
64
|
+
margin-top: ${(p) => p.theme.spacing.xs};
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const ErrorListWrapper = styled.ul`
|
|
68
|
+
margin-top: ${(p) => p.theme.spacing.xs};
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
ErrorsWrapper,
|
|
73
|
+
ErrorsHeader,
|
|
74
|
+
IconWrapper,
|
|
75
|
+
WarningWrapper,
|
|
76
|
+
HeaderText,
|
|
77
|
+
HeaderActions,
|
|
78
|
+
ErrorsContent,
|
|
79
|
+
ToggleWrapper,
|
|
80
|
+
Description,
|
|
81
|
+
ErrorListWrapper,
|
|
82
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Icon } from "@ax/components";
|
|
2
|
+
import type { HeadingNode } from "@ax/types";
|
|
3
|
+
|
|
4
|
+
import * as S from "./style";
|
|
5
|
+
|
|
6
|
+
const HeadingItem = ({
|
|
7
|
+
head,
|
|
8
|
+
index,
|
|
9
|
+
isFiltering,
|
|
10
|
+
selected,
|
|
11
|
+
onHeadingClick,
|
|
12
|
+
counter,
|
|
13
|
+
parentPath,
|
|
14
|
+
}: IHeadingItemProps) => {
|
|
15
|
+
counter.value += 1;
|
|
16
|
+
const headingId = counter.value;
|
|
17
|
+
const uniqueKey = `${parentPath}${head.tag}-${head.level}-${index}-${head.text.slice(0, 20)}`;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<S.HeadItem
|
|
22
|
+
level={isFiltering ? 1 : head.level}
|
|
23
|
+
tag={head.tag}
|
|
24
|
+
tabIndex={0}
|
|
25
|
+
role="button"
|
|
26
|
+
onClick={onHeadingClick(headingId)}
|
|
27
|
+
isSelected={selected === headingId}
|
|
28
|
+
isHidden={head.isHidden}
|
|
29
|
+
data-heading-id={headingId}
|
|
30
|
+
>
|
|
31
|
+
<S.HeadTag>
|
|
32
|
+
<div>{head.tag}</div>
|
|
33
|
+
</S.HeadTag>
|
|
34
|
+
<S.StyledTooltip content={head.isHidden ? "Hidden with CSS" : null}>
|
|
35
|
+
<S.HeadText>
|
|
36
|
+
<div>{head.text}</div>
|
|
37
|
+
{head.isHidden && (
|
|
38
|
+
<S.HiddenIcon>
|
|
39
|
+
<Icon name="hide" size="16" />
|
|
40
|
+
</S.HiddenIcon>
|
|
41
|
+
)}
|
|
42
|
+
</S.HeadText>
|
|
43
|
+
</S.StyledTooltip>
|
|
44
|
+
</S.HeadItem>
|
|
45
|
+
{head.children.map((child, childIndex) => (
|
|
46
|
+
<HeadingItem
|
|
47
|
+
key={`${uniqueKey}-child-${childIndex}`}
|
|
48
|
+
head={child}
|
|
49
|
+
index={childIndex}
|
|
50
|
+
isFiltering={isFiltering}
|
|
51
|
+
selected={selected}
|
|
52
|
+
onHeadingClick={onHeadingClick}
|
|
53
|
+
counter={counter}
|
|
54
|
+
parentPath={`${uniqueKey}-`}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
interface IHeadingItemProps {
|
|
62
|
+
head: HeadingNode;
|
|
63
|
+
index: number;
|
|
64
|
+
isFiltering: boolean;
|
|
65
|
+
selected: number | null;
|
|
66
|
+
onHeadingClick: (id: number) => () => void;
|
|
67
|
+
counter: { value: number };
|
|
68
|
+
parentPath: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default HeadingItem;
|