@ndla/ui 4.3.1 → 5.0.1
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/README.md +11 -11
- package/es/Article/Article.js +3 -7
- package/es/Article/ArticleByline.js +4 -4
- package/es/Article/ArticleNotions.js +37 -92
- package/es/ContentTypeBadge/ContentTypeBadge.js +10 -1
- package/es/FactBox/FactBox.js +36 -11
- package/es/Filter/FilterButtons.js +18 -17
- package/es/Filter/FilterCarousel.js +198 -0
- package/es/Masthead/Masthead.js +24 -2
- package/es/Notion/ConceptNotion.js +57 -0
- package/es/Notion/FigureNotion.js +84 -0
- package/es/Notion/Notion.js +122 -48
- package/es/Notion/NotionImage.js +47 -0
- package/es/Notion/NotionVisualElement.js +38 -0
- package/es/Notion/index.js +2 -1
- package/es/SearchTypeResult/ActiveFilterContent.js +17 -2
- package/es/SearchTypeResult/ActiveFilters.js +64 -50
- package/es/SearchTypeResult/PopupFilter.js +28 -125
- package/es/SearchTypeResult/ResultNavigation.js +41 -14
- package/es/SearchTypeResult/SearchFieldHeader.js +20 -41
- package/es/SearchTypeResult/SearchFilterContent.js +61 -0
- package/es/SearchTypeResult/SearchHeader.js +51 -28
- package/es/SearchTypeResult/SearchItem.js +64 -190
- package/es/SearchTypeResult/SearchItemList.js +132 -0
- package/es/SearchTypeResult/SearchItems.js +17 -13
- package/es/SearchTypeResult/SearchNotionItem.js +13 -13
- package/es/SearchTypeResult/SearchNotionsResult.js +16 -21
- package/es/SearchTypeResult/SearchTypeHeader.js +43 -26
- package/es/SearchTypeResult/SearchTypeResult.js +7 -5
- package/es/SearchTypeResult/SearchViewType.js +93 -0
- package/es/SearchTypeResult/components/ItemContexts.js +125 -0
- package/es/SearchTypeResult/components/ItemResourceHeader.js +87 -0
- package/es/SearchTypeResult/components/ItemTopicHeader.js +56 -0
- package/es/SearchTypeResult/components/SubjectFilters.js +177 -0
- package/es/SearchTypeResult/index.js +2 -1
- package/es/all.css +1 -1
- package/es/index.js +3 -2
- package/es/locale/messages-en.js +21 -6
- package/es/locale/messages-nb.js +21 -6
- package/es/locale/messages-nn.js +21 -6
- package/es/model/ContentType.js +2 -1
- package/es/shapes.js +1 -1
- package/lib/Article/Article.d.ts +4 -5
- package/lib/Article/Article.js +3 -7
- package/lib/Article/ArticleByline.js +4 -4
- package/lib/Article/ArticleNotions.d.ts +3 -8
- package/lib/Article/ArticleNotions.js +41 -90
- package/lib/ContentTypeBadge/ContentTypeBadge.d.ts +1 -0
- package/lib/ContentTypeBadge/ContentTypeBadge.js +14 -2
- package/lib/FactBox/FactBox.js +41 -8
- package/lib/Filter/FilterButtons.d.ts +3 -10
- package/lib/Filter/FilterButtons.js +19 -17
- package/lib/Filter/FilterCarousel.d.ts +13 -0
- package/lib/Filter/FilterCarousel.js +207 -0
- package/lib/Masthead/Masthead.js +30 -2
- package/lib/Notion/ConceptNotion.d.ts +25 -0
- package/lib/Notion/ConceptNotion.js +79 -0
- package/lib/Notion/FigureNotion.d.ts +23 -0
- package/lib/Notion/FigureNotion.js +99 -0
- package/lib/Notion/Notion.d.ts +24 -11
- package/lib/Notion/Notion.js +120 -48
- package/lib/Notion/NotionImage.d.ts +15 -0
- package/lib/Notion/NotionImage.js +63 -0
- package/lib/Notion/NotionVisualElement.d.ts +22 -0
- package/lib/Notion/NotionVisualElement.js +51 -0
- package/lib/Notion/index.d.ts +1 -0
- package/lib/Notion/index.js +8 -0
- package/lib/SearchTypeResult/ActiveFilterContent.js +16 -9
- package/lib/SearchTypeResult/ActiveFilters.d.ts +2 -1
- package/lib/SearchTypeResult/ActiveFilters.js +65 -50
- package/lib/SearchTypeResult/PopupFilter.d.ts +13 -19
- package/lib/SearchTypeResult/PopupFilter.js +27 -123
- package/lib/SearchTypeResult/ResultNavigation.d.ts +2 -2
- package/lib/SearchTypeResult/ResultNavigation.js +38 -14
- package/lib/SearchTypeResult/SearchFieldHeader.d.ts +3 -8
- package/lib/SearchTypeResult/SearchFieldHeader.js +18 -39
- package/lib/SearchTypeResult/SearchFilterContent.d.ts +16 -0
- package/lib/SearchTypeResult/SearchFilterContent.js +67 -0
- package/lib/SearchTypeResult/SearchHeader.d.ts +3 -7
- package/lib/SearchTypeResult/SearchHeader.js +59 -30
- package/lib/SearchTypeResult/SearchItem.d.ts +8 -12
- package/lib/SearchTypeResult/SearchItem.js +64 -187
- package/lib/SearchTypeResult/SearchItemList.d.ts +10 -0
- package/lib/SearchTypeResult/SearchItemList.js +139 -0
- package/lib/SearchTypeResult/SearchItems.d.ts +4 -3
- package/lib/SearchTypeResult/SearchItems.js +18 -13
- package/lib/SearchTypeResult/SearchNotionItem.js +13 -13
- package/lib/SearchTypeResult/SearchNotionsResult.d.ts +2 -4
- package/lib/SearchTypeResult/SearchNotionsResult.js +23 -23
- package/lib/SearchTypeResult/SearchTypeHeader.d.ts +2 -2
- package/lib/SearchTypeResult/SearchTypeHeader.js +40 -25
- package/lib/SearchTypeResult/SearchTypeResult.d.ts +7 -6
- package/lib/SearchTypeResult/SearchTypeResult.js +7 -5
- package/lib/SearchTypeResult/SearchViewType.d.ts +13 -0
- package/lib/SearchTypeResult/SearchViewType.js +99 -0
- package/lib/SearchTypeResult/components/ItemContexts.d.ts +19 -0
- package/lib/SearchTypeResult/components/ItemContexts.js +134 -0
- package/lib/SearchTypeResult/components/ItemResourceHeader.d.ts +16 -0
- package/lib/SearchTypeResult/components/ItemResourceHeader.js +98 -0
- package/lib/SearchTypeResult/components/ItemTopicHeader.d.ts +17 -0
- package/lib/SearchTypeResult/components/ItemTopicHeader.js +67 -0
- package/lib/SearchTypeResult/components/SubjectFilters.d.ts +32 -0
- package/lib/SearchTypeResult/components/SubjectFilters.js +192 -0
- package/lib/SearchTypeResult/index.d.ts +2 -1
- package/lib/SearchTypeResult/index.js +8 -0
- package/lib/all.css +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +24 -1
- package/lib/locale/messages-en.d.ts +16 -1
- package/lib/locale/messages-en.js +21 -6
- package/lib/locale/messages-nb.d.ts +15 -0
- package/lib/locale/messages-nb.js +21 -6
- package/lib/locale/messages-nn.d.ts +16 -1
- package/lib/locale/messages-nn.js +21 -6
- package/lib/model/ContentType.d.ts +1 -0
- package/lib/model/ContentType.js +4 -2
- package/lib/shapes.js +1 -1
- package/lib/types.d.ts +1 -0
- package/package.json +14 -14
- package/src/Article/Article.tsx +8 -15
- package/src/Article/ArticleByline.tsx +1 -1
- package/src/Article/ArticleNotions.tsx +13 -33
- package/src/ContentTypeBadge/ContentTypeBadge.tsx +8 -0
- package/src/ContentTypeBadge/component.content-type-badge.scss +5 -0
- package/src/FactBox/FactBox.tsx +22 -15
- package/src/Figure/component.figure.scss +1 -1
- package/src/Filter/FilterButtons.tsx +14 -15
- package/src/Filter/FilterCarousel.tsx +166 -0
- package/src/Masthead/Masthead.tsx +42 -18
- package/src/Notion/ConceptNotion.tsx +80 -0
- package/src/Notion/FigureNotion.tsx +88 -0
- package/src/Notion/Notion.tsx +205 -75
- package/src/Notion/NotionImage.tsx +51 -0
- package/src/Notion/NotionVisualElement.tsx +50 -0
- package/src/Notion/index.ts +1 -0
- package/src/SearchTypeResult/ActiveFilterContent.tsx +7 -2
- package/src/SearchTypeResult/ActiveFilters.tsx +72 -38
- package/src/SearchTypeResult/PopupFilter.tsx +73 -146
- package/src/SearchTypeResult/ResultNavigation.tsx +54 -16
- package/src/SearchTypeResult/SearchFieldHeader.tsx +15 -40
- package/src/SearchTypeResult/SearchFilterContent.tsx +63 -0
- package/src/SearchTypeResult/SearchHeader.tsx +31 -31
- package/src/SearchTypeResult/SearchItem.tsx +145 -233
- package/src/SearchTypeResult/SearchItemList.tsx +167 -0
- package/src/SearchTypeResult/SearchItems.tsx +26 -19
- package/src/SearchTypeResult/SearchNotionItem.tsx +5 -12
- package/src/SearchTypeResult/SearchNotionsResult.tsx +29 -22
- package/src/SearchTypeResult/SearchTypeHeader.tsx +51 -33
- package/src/SearchTypeResult/SearchTypeResult.tsx +13 -12
- package/src/SearchTypeResult/SearchViewType.tsx +109 -0
- package/src/SearchTypeResult/components/ItemContexts.tsx +138 -0
- package/src/SearchTypeResult/components/ItemResourceHeader.tsx +133 -0
- package/src/SearchTypeResult/components/ItemTopicHeader.tsx +95 -0
- package/src/SearchTypeResult/components/SubjectFilters.tsx +152 -0
- package/src/SearchTypeResult/index.ts +9 -1
- package/src/index.ts +3 -0
- package/src/locale/messages-en.ts +19 -4
- package/src/locale/messages-nb.ts +19 -4
- package/src/locale/messages-nn.ts +19 -4
- package/src/model/ContentType.ts +1 -0
- package/src/shapes.js +1 -0
- package/src/types.ts +1 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2021-present, NDLA.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { Fragment } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import parse from 'html-react-parser';
|
|
12
|
+
import styled from '@emotion/styled';
|
|
13
|
+
|
|
14
|
+
import { animations, colors, fonts, spacing } from '@ndla/core';
|
|
15
|
+
import SafeLink from '@ndla/safelink';
|
|
16
|
+
|
|
17
|
+
import ContentTypeBadge from '../ContentTypeBadge';
|
|
18
|
+
import ItemContexts from './components/ItemContexts';
|
|
19
|
+
import { SearchItemType } from './SearchItem';
|
|
20
|
+
import constants from '../model';
|
|
21
|
+
const { contentTypes } = constants;
|
|
22
|
+
|
|
23
|
+
const Container = styled.div`
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
min-height: 192px;
|
|
27
|
+
height: 100%;
|
|
28
|
+
border: 1px solid ${colors.brand.neutral7};
|
|
29
|
+
border-radius: 5px;
|
|
30
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
31
|
+
&:hover {
|
|
32
|
+
margin: -2px;
|
|
33
|
+
min-height: 196px;
|
|
34
|
+
height: calc(100% + 4px);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const ItemLink = styled(SafeLink)`
|
|
39
|
+
box-shadow: none;
|
|
40
|
+
color: unset;
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
display: flex;
|
|
43
|
+
position: relative;
|
|
44
|
+
min-height: 0;
|
|
45
|
+
flex: 1;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const TextWrapper = styled.div`
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
flex: 1;
|
|
53
|
+
margin: ${spacing.small} ${spacing.small} ${spacing.small} ${spacing.normal};
|
|
54
|
+
max-width: 800px;
|
|
55
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
56
|
+
${Container}:hover & {
|
|
57
|
+
margin: calc(${spacing.small} + 2px) calc(${spacing.small} + 2px) calc(${spacing.small} + 2px)
|
|
58
|
+
calc(${spacing.normal} + 2px);
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
const ContentTypeWrapper = styled.div`
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
${fonts.sizes('12px', '16px')};
|
|
65
|
+
font-weight: ${fonts.weight.semibold};
|
|
66
|
+
`;
|
|
67
|
+
const ContentTypeIconWrapper = styled.div`
|
|
68
|
+
margin-right: ${spacing.small};
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const ItemTitleWrapper = styled.div`
|
|
72
|
+
margin: ${spacing.small} 0 ${spacing.xsmall};
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const ItemTitle = styled.h3`
|
|
76
|
+
${fonts.sizes('24px', '28px')};
|
|
77
|
+
color: ${colors.brand.primary};
|
|
78
|
+
font-weight: ${fonts.weight.semibold};
|
|
79
|
+
overflow-wrap: anywhere;
|
|
80
|
+
display: inline;
|
|
81
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
82
|
+
${Container}:hover & {
|
|
83
|
+
box-shadow: inset 0 -1px;
|
|
84
|
+
background-color: transparent;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const ItemText = styled.div`
|
|
89
|
+
${fonts.sizes('16px', '24px')};
|
|
90
|
+
flex: 1 auto;
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const ImageWrapper = styled.div<{ isTopic: boolean }>`
|
|
94
|
+
float: right;
|
|
95
|
+
position: relative;
|
|
96
|
+
width: ${(props) => (props.isTopic ? `164px` : `224px`)};
|
|
97
|
+
height: 164px;
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
align-items: center;
|
|
101
|
+
margin: ${spacing.small};
|
|
102
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
103
|
+
${Container}:hover & {
|
|
104
|
+
width: ${(props) => (props.isTopic ? `168px` : `228px`)};
|
|
105
|
+
height: 168px;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const ImageElement = styled.img<{ isTopic: boolean }>`
|
|
110
|
+
border-radius: 2px;
|
|
111
|
+
width: 100%;
|
|
112
|
+
height: 100%;
|
|
113
|
+
object-fit: cover;
|
|
114
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
115
|
+
max-width: unset;
|
|
116
|
+
${(props) => props.isTopic && `border-radius: 50%;`};
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
const ContextWrapper = styled.div`
|
|
120
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const SearchItemList = ({ item, type }: SearchItemType) => {
|
|
124
|
+
const { t } = useTranslation();
|
|
125
|
+
const { id, title, url, ingress, contexts, img = null, labels = [] } = item;
|
|
126
|
+
const isTopic = type === contentTypes.TOPIC || type === contentTypes.MULTIDISCIPLINARY_TOPIC;
|
|
127
|
+
return (
|
|
128
|
+
<Container>
|
|
129
|
+
<ItemLink to={url}>
|
|
130
|
+
<TextWrapper>
|
|
131
|
+
{type && (
|
|
132
|
+
<ContentTypeWrapper>
|
|
133
|
+
<ContentTypeIconWrapper>
|
|
134
|
+
<ContentTypeBadge type={type} background border={false} />
|
|
135
|
+
</ContentTypeIconWrapper>
|
|
136
|
+
{t(`contentTypes.${type}`)}
|
|
137
|
+
{labels.length > 0 && (
|
|
138
|
+
<>
|
|
139
|
+
{labels.map((label) => (
|
|
140
|
+
<Fragment key={label}>
|
|
141
|
+
{' '}
|
|
142
|
+
<>•</> {label}
|
|
143
|
+
</Fragment>
|
|
144
|
+
))}
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</ContentTypeWrapper>
|
|
148
|
+
)}
|
|
149
|
+
<ItemTitleWrapper>
|
|
150
|
+
<ItemTitle>{title}</ItemTitle>
|
|
151
|
+
</ItemTitleWrapper>
|
|
152
|
+
<ItemText>{parse(ingress)}</ItemText>
|
|
153
|
+
<ContextWrapper>
|
|
154
|
+
{contexts && contexts.length > 0 && <ItemContexts contexts={contexts} id={id} title={title} />}
|
|
155
|
+
</ContextWrapper>
|
|
156
|
+
</TextWrapper>
|
|
157
|
+
{img && (
|
|
158
|
+
<ImageWrapper isTopic={isTopic}>
|
|
159
|
+
<ImageElement src={img.url} alt={img.alt} isTopic={isTopic} />
|
|
160
|
+
</ImageWrapper>
|
|
161
|
+
)}
|
|
162
|
+
</ItemLink>
|
|
163
|
+
</Container>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export default SearchItemList;
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
import React, { memo } from 'react';
|
|
10
10
|
import styled from '@emotion/styled';
|
|
11
11
|
import { breakpoints, mq, spacing } from '@ndla/core';
|
|
12
|
-
import SearchItem, {
|
|
12
|
+
import SearchItem, { SearchItemProps } from './SearchItem';
|
|
13
13
|
import { ContentType } from './SearchTypeResult';
|
|
14
|
+
import SearchItemList from './SearchItemList';
|
|
14
15
|
|
|
15
16
|
const Wrapper = styled.div`
|
|
16
17
|
width: 100%;
|
|
@@ -18,38 +19,44 @@ const Wrapper = styled.div`
|
|
|
18
19
|
`;
|
|
19
20
|
|
|
20
21
|
type ContainerProps = {
|
|
21
|
-
|
|
22
|
-
type?: string;
|
|
22
|
+
viewType: Props['viewType'];
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const Container = styled.div<ContainerProps>`
|
|
26
26
|
display: grid;
|
|
27
27
|
row-gap: ${spacing.normal};
|
|
28
28
|
grid-template-columns: repeat(1, 1fr);
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
${(props) =>
|
|
31
|
+
props.viewType === 'grid' &&
|
|
32
|
+
`
|
|
33
|
+
${mq.range({ from: breakpoints.tablet })} {
|
|
30
34
|
column-gap: ${spacing.normal};
|
|
31
35
|
grid-template-columns: repeat(2, 1fr);
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
grid-template-columns: repeat(3, 1fr);
|
|
35
|
-
}
|
|
37
|
+
|
|
36
38
|
${mq.range({ from: breakpoints.desktop })} {
|
|
37
|
-
grid-template-columns: repeat(
|
|
38
|
-
}
|
|
39
|
+
grid-template-columns: repeat(3, 1fr);
|
|
40
|
+
}`}
|
|
39
41
|
`;
|
|
40
42
|
|
|
41
43
|
type Props = {
|
|
42
|
-
items:
|
|
44
|
+
items: SearchItemProps[];
|
|
43
45
|
type?: ContentType;
|
|
46
|
+
viewType?: 'grid' | 'list';
|
|
47
|
+
};
|
|
48
|
+
const SearchItems = ({ items, type, viewType = 'grid' }: Props) => {
|
|
49
|
+
return (
|
|
50
|
+
<Wrapper>
|
|
51
|
+
<Container viewType={viewType}>
|
|
52
|
+
{items.map((item) => {
|
|
53
|
+
const contentType = type || item.type;
|
|
54
|
+
const Component = viewType === 'list' ? SearchItemList : SearchItem;
|
|
55
|
+
return <Component item={item} key={`${item.id}`} type={contentType} />;
|
|
56
|
+
})}
|
|
57
|
+
</Container>
|
|
58
|
+
</Wrapper>
|
|
59
|
+
);
|
|
44
60
|
};
|
|
45
|
-
const SearchItems = ({ items, type }: Props) => (
|
|
46
|
-
<Wrapper>
|
|
47
|
-
<Container itemCount={items.length} type={type}>
|
|
48
|
-
{items.map((item: any) => (
|
|
49
|
-
<SearchItem item={item} key={`${item.id}`} type={type} />
|
|
50
|
-
))}
|
|
51
|
-
</Container>
|
|
52
|
-
</Wrapper>
|
|
53
|
-
);
|
|
54
61
|
|
|
55
62
|
export default memo(SearchItems);
|
|
@@ -148,18 +148,11 @@ const SearchNotionItem = ({
|
|
|
148
148
|
<Modal
|
|
149
149
|
activateButton={
|
|
150
150
|
<Button lighter size="xsmall" borderShape="rounded">
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)}
|
|
157
|
-
{type === 'other' && (
|
|
158
|
-
<>
|
|
159
|
-
<ArrowExpand />
|
|
160
|
-
<ShowMediaButtonText>{t('searchPage.resultType.showNotion')}</ShowMediaButtonText>
|
|
161
|
-
</>
|
|
162
|
-
)}
|
|
151
|
+
<>
|
|
152
|
+
{type === 'video' && <Play />}
|
|
153
|
+
{type === 'other' && <ArrowExpand />}
|
|
154
|
+
<ShowMediaButtonText>{t('searchPage.resultType.showVideo')}</ShowMediaButtonText>
|
|
155
|
+
</>
|
|
163
156
|
</Button>
|
|
164
157
|
}
|
|
165
158
|
animation="subtle"
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { ReactNode } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
10
11
|
import styled from '@emotion/styled';
|
|
11
|
-
import { fonts, mq, breakpoints, spacing } from '@ndla/core';
|
|
12
|
+
import { fonts, mq, breakpoints, spacing, colors } from '@ndla/core';
|
|
12
13
|
import { ModalCloseButton } from '@ndla/modal';
|
|
13
14
|
import { withTranslation, WithTranslation } from 'react-i18next';
|
|
14
|
-
import SearchNotionItem, { SearchNotionItemProps } from './SearchNotionItem';
|
|
15
15
|
|
|
16
16
|
const Wrapper = styled.div`
|
|
17
17
|
display: flex;
|
|
@@ -19,12 +19,18 @@ const Wrapper = styled.div`
|
|
|
19
19
|
padding: ${spacing.medium} 0;
|
|
20
20
|
${mq.range({ from: breakpoints.tablet })} {
|
|
21
21
|
padding: ${spacing.medium};
|
|
22
|
-
|
|
23
|
-
border-radius:
|
|
22
|
+
border: 1px solid ${colors.brand.neutral7};
|
|
23
|
+
border-radius: 4px;
|
|
24
24
|
margin: ${spacing.large} 0;
|
|
25
25
|
}
|
|
26
26
|
${mq.range({ from: breakpoints.desktop })} {
|
|
27
|
-
padding: ${spacing.
|
|
27
|
+
padding: ${spacing.large};
|
|
28
|
+
}
|
|
29
|
+
& > .c-figure {
|
|
30
|
+
width: 100% !important;
|
|
31
|
+
padding: 0;
|
|
32
|
+
left: initial !important;
|
|
33
|
+
margin-bottom: 0;
|
|
28
34
|
}
|
|
29
35
|
`;
|
|
30
36
|
|
|
@@ -32,7 +38,8 @@ const HeadingWrapper = styled.div`
|
|
|
32
38
|
display: flex;
|
|
33
39
|
justify-content: space-between;
|
|
34
40
|
align-items: center;
|
|
35
|
-
|
|
41
|
+
border-bottom: 2px solid ${colors.brand.light};
|
|
42
|
+
padding-bottom: ${spacing.small};
|
|
36
43
|
`;
|
|
37
44
|
const Heading = styled.h2`
|
|
38
45
|
margin: 0;
|
|
@@ -47,25 +54,25 @@ const HeadingCount = styled.span`
|
|
|
47
54
|
`;
|
|
48
55
|
|
|
49
56
|
type Props = {
|
|
50
|
-
items: SearchNotionItemProps[];
|
|
51
57
|
totalCount: number;
|
|
52
58
|
onRemove: () => void;
|
|
53
|
-
|
|
59
|
+
children: ReactNode;
|
|
54
60
|
};
|
|
55
61
|
|
|
56
|
-
const SearchNotionsResult = ({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
62
|
+
const SearchNotionsResult = ({ totalCount, onRemove, children }: Props & WithTranslation) => {
|
|
63
|
+
const { t } = useTranslation();
|
|
64
|
+
return (
|
|
65
|
+
<Wrapper>
|
|
66
|
+
<HeadingWrapper>
|
|
67
|
+
<Heading>
|
|
68
|
+
{t(`searchPage.resultType.notionsHeading`)}
|
|
69
|
+
<HeadingCount>{t(`searchPage.resultType.hits`, { count: totalCount })}</HeadingCount>
|
|
70
|
+
</Heading>
|
|
71
|
+
<ModalCloseButton onClick={onRemove} title={t(`searchPage.resultType.notionsRemove`)} />
|
|
72
|
+
</HeadingWrapper>
|
|
73
|
+
{children}
|
|
74
|
+
</Wrapper>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
70
77
|
|
|
71
78
|
export default withTranslation()(SearchNotionsResult);
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
import React, { MouseEvent, memo } from 'react'; // useMemo , { Children }
|
|
10
10
|
import styled from '@emotion/styled';
|
|
11
11
|
import Button from '@ndla/button';
|
|
12
|
-
import { breakpoints, mq, spacing } from '@ndla/core';
|
|
13
|
-
import {
|
|
12
|
+
import { breakpoints, fonts, mq, spacing } from '@ndla/core';
|
|
13
|
+
import { withTranslation, WithTranslation } from 'react-i18next';
|
|
14
|
+
import { Cross } from '@ndla/icons/action';
|
|
14
15
|
import ContentTypeBadge from '../ContentTypeBadge';
|
|
15
16
|
import { ContentType } from './SearchTypeResult';
|
|
16
17
|
|
|
18
|
+
const Wrapper = styled.div`
|
|
19
|
+
margin: ${spacing.small} 0;
|
|
20
|
+
`;
|
|
17
21
|
const HeaderWrapper = styled.div`
|
|
18
22
|
display: flex;
|
|
19
23
|
flex-direction: row;
|
|
20
|
-
border-bottom: 2px solid #20588f;
|
|
21
24
|
align-items: center;
|
|
22
|
-
margin: ${spacing.small} 0;
|
|
23
25
|
justify-content: space-between;
|
|
24
26
|
padding: 0 2px;
|
|
25
27
|
${mq.range({ until: breakpoints.tablet })} {
|
|
@@ -36,36 +38,39 @@ const TypeWrapper = styled.div`
|
|
|
36
38
|
margin-bottom: ${spacing.xsmall};
|
|
37
39
|
`;
|
|
38
40
|
|
|
41
|
+
const BadgeWrapper = styled.span`
|
|
42
|
+
margin-right: ${spacing.small};
|
|
43
|
+
`;
|
|
44
|
+
|
|
39
45
|
const SubjectName = styled.span`
|
|
40
|
-
|
|
41
|
-
margin: 2px
|
|
46
|
+
${fonts.sizes('18px', '24px')};
|
|
47
|
+
margin: 2px 0;
|
|
42
48
|
b {
|
|
43
|
-
|
|
49
|
+
${fonts.sizes('18px', '24px')};
|
|
44
50
|
margin-right: 4px;
|
|
45
|
-
font-weight: 600;
|
|
46
51
|
}
|
|
47
52
|
`;
|
|
48
53
|
|
|
49
54
|
const Count = styled.span``;
|
|
50
55
|
|
|
51
|
-
const CategoryTypeButtonWrapper = styled.div`
|
|
52
|
-
margin: 4px;
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
56
|
const CategoryItems = styled.div`
|
|
56
57
|
position: relative;
|
|
57
|
-
right: -4px;
|
|
58
58
|
button {
|
|
59
59
|
white-space: nowrap;
|
|
60
60
|
max-height: 29px;
|
|
61
61
|
}
|
|
62
62
|
white-space: nowrap;
|
|
63
63
|
display: flex;
|
|
64
|
-
justify-content: flex-end;
|
|
65
64
|
flex-wrap: wrap;
|
|
66
|
-
${
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
margin: ${spacing.small} 0 0;
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const CategoryTypeButtonWrapper = styled.div`
|
|
69
|
+
margin: 4px;
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const CategoryTypeCrossWrapper = styled.span`
|
|
73
|
+
margin-left: ${spacing.xsmall};
|
|
69
74
|
`;
|
|
70
75
|
|
|
71
76
|
export type FilterOptionsType = {
|
|
@@ -75,39 +80,52 @@ export type FilterOptionsType = {
|
|
|
75
80
|
};
|
|
76
81
|
|
|
77
82
|
type Props = {
|
|
78
|
-
filters
|
|
79
|
-
onFilterClick
|
|
83
|
+
filters?: FilterOptionsType[];
|
|
84
|
+
onFilterClick?: (id: string) => void;
|
|
80
85
|
totalCount: number;
|
|
81
86
|
type?: ContentType;
|
|
82
87
|
};
|
|
83
|
-
const SearchTypeHeader = ({ filters, onFilterClick, totalCount, type, t }: Props & WithTranslation) => (
|
|
84
|
-
<
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
const SearchTypeHeader = ({ filters = [], onFilterClick, totalCount, type, t }: Props & WithTranslation) => (
|
|
89
|
+
<Wrapper>
|
|
90
|
+
<HeaderWrapper>
|
|
91
|
+
<TypeWrapper>
|
|
92
|
+
{type && (
|
|
93
|
+
<BadgeWrapper>
|
|
94
|
+
<ContentTypeBadge type={type} title={t(`contentTypes.${type}`)} background border={false} size="large" />
|
|
95
|
+
</BadgeWrapper>
|
|
96
|
+
)}
|
|
97
|
+
<SubjectName>
|
|
98
|
+
<b>{type ? t(`contentTypes.${type}`) : t(`searchPage.resultType.allContentTypes`)}</b>{' '}
|
|
99
|
+
{totalCount && <Count>{t(`searchPage.resultType.hits`, { count: totalCount })}</Count>}
|
|
100
|
+
</SubjectName>
|
|
101
|
+
</TypeWrapper>
|
|
102
|
+
</HeaderWrapper>
|
|
103
|
+
{filters.length > 0 && (
|
|
93
104
|
<CategoryItems>
|
|
94
105
|
{filters.map((option: FilterOptionsType) => (
|
|
95
106
|
<CategoryTypeButtonWrapper key={option.id}>
|
|
96
107
|
<Button
|
|
97
|
-
size="
|
|
98
|
-
|
|
108
|
+
size="xsmall"
|
|
109
|
+
borderShape="rounded"
|
|
110
|
+
greyLighter={!option.active}
|
|
99
111
|
onClick={(e: MouseEvent<HTMLButtonElement>) => {
|
|
100
112
|
if (e.currentTarget && option.active) {
|
|
101
113
|
e.currentTarget.blur();
|
|
102
114
|
}
|
|
103
|
-
onFilterClick(option.id);
|
|
115
|
+
onFilterClick?.(option.id);
|
|
104
116
|
}}>
|
|
105
117
|
{option.name}
|
|
118
|
+
{option.active && (
|
|
119
|
+
<CategoryTypeCrossWrapper>
|
|
120
|
+
<Cross />
|
|
121
|
+
</CategoryTypeCrossWrapper>
|
|
122
|
+
)}
|
|
106
123
|
</Button>
|
|
107
124
|
</CategoryTypeButtonWrapper>
|
|
108
125
|
))}
|
|
109
126
|
</CategoryItems>
|
|
110
127
|
)}
|
|
111
|
-
</
|
|
128
|
+
</Wrapper>
|
|
112
129
|
);
|
|
130
|
+
|
|
113
131
|
export default memo(withTranslation()(SearchTypeHeader));
|
|
@@ -13,7 +13,7 @@ import Spinner from '../Spinner';
|
|
|
13
13
|
import constants from '../model';
|
|
14
14
|
import SearchTypeHeader, { FilterOptionsType } from './SearchTypeHeader';
|
|
15
15
|
import SearchItems from './SearchItems';
|
|
16
|
-
import {
|
|
16
|
+
import { SearchItemProps } from './SearchItem';
|
|
17
17
|
import ResultNavigation, { PaginationType } from './ResultNavigation';
|
|
18
18
|
|
|
19
19
|
const Wrapper = styled.div`
|
|
@@ -47,17 +47,19 @@ export type ContentType =
|
|
|
47
47
|
| typeof constants.contentTypes.EXTERNAL_LEARNING_RESOURCES
|
|
48
48
|
| typeof constants.contentTypes.SOURCE_MATERIAL
|
|
49
49
|
| typeof constants.contentTypes.LEARNING_PATH
|
|
50
|
-
| typeof constants.contentTypes.TOPIC
|
|
50
|
+
| typeof constants.contentTypes.TOPIC
|
|
51
|
+
| typeof constants.contentTypes.MULTIDISCIPLINARY_TOPIC;
|
|
51
52
|
|
|
52
53
|
type Props = {
|
|
53
|
-
items:
|
|
54
|
-
filters
|
|
55
|
-
onFilterClick
|
|
54
|
+
items: SearchItemProps[];
|
|
55
|
+
filters?: FilterOptionsType[];
|
|
56
|
+
onFilterClick?: (id: string) => void;
|
|
56
57
|
totalCount?: number;
|
|
57
58
|
pagination?: PaginationType;
|
|
58
59
|
type?: ContentType;
|
|
59
60
|
loading?: boolean;
|
|
60
61
|
children?: ReactNode;
|
|
62
|
+
viewType?: 'grid' | 'list';
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
const SearchTypeResult = ({
|
|
@@ -68,20 +70,19 @@ const SearchTypeResult = ({
|
|
|
68
70
|
pagination,
|
|
69
71
|
type,
|
|
70
72
|
loading,
|
|
73
|
+
viewType,
|
|
71
74
|
children,
|
|
72
75
|
}: Props) => (
|
|
73
76
|
<Wrapper>
|
|
74
77
|
{loading && (
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
</Overlay>
|
|
79
|
-
</>
|
|
78
|
+
<Overlay>
|
|
79
|
+
<Spinner />
|
|
80
|
+
</Overlay>
|
|
80
81
|
)}
|
|
81
82
|
<SearchTypeHeader onFilterClick={onFilterClick} filters={filters} totalCount={totalCount} type={type} />
|
|
82
|
-
<SearchItems items={items} type={type} />
|
|
83
|
+
<SearchItems items={items} type={type} viewType={viewType} />
|
|
83
84
|
{pagination && <ResultNavigation {...pagination} />}
|
|
84
|
-
{children
|
|
85
|
+
{children}
|
|
85
86
|
</Wrapper>
|
|
86
87
|
);
|
|
87
88
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2021-present, NDLA.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import styled from '@emotion/styled';
|
|
11
|
+
import Button from '@ndla/button';
|
|
12
|
+
import { Grid } from '@ndla/icons/common';
|
|
13
|
+
import { ListCircle } from '@ndla/icons/editor';
|
|
14
|
+
import { animations, breakpoints, colors, mq } from '@ndla/core';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
16
|
+
|
|
17
|
+
const Wrapper = styled.div`
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
flex: 1;
|
|
22
|
+
${mq.range({ until: breakpoints.tablet })} {
|
|
23
|
+
display: none;
|
|
24
|
+
}
|
|
25
|
+
padding-top: 8px;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const ButtonContainer = styled.div`
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
align-items: center;
|
|
32
|
+
width: 44px;
|
|
33
|
+
height: 44px;
|
|
34
|
+
`;
|
|
35
|
+
const ButtonElement = styled(Button)`
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
padding: 2px;
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: 100%;
|
|
40
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
41
|
+
&:hover {
|
|
42
|
+
padding: 0;
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
type ButtonContentProps = {
|
|
47
|
+
selected?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ButtonContent = styled.span<ButtonContentProps>`
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
align-items: center;
|
|
56
|
+
color: ${colors.brand.primary};
|
|
57
|
+
${(props) => props.selected && `background: ${colors.brand.greyLighter};`}
|
|
58
|
+
border-radius: 50%;
|
|
59
|
+
transition: all ${animations.durations.fast} ease-in-out;
|
|
60
|
+
${ButtonElement}:hover & {
|
|
61
|
+
background: ${colors.brand.greyLighter};
|
|
62
|
+
}
|
|
63
|
+
svg {
|
|
64
|
+
width: 24px;
|
|
65
|
+
height: 24px;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
export type SearchViewTypeProps = {
|
|
70
|
+
viewType: 'grid' | 'list';
|
|
71
|
+
onChangeViewType: (viewType: SearchViewTypeProps['viewType']) => void;
|
|
72
|
+
};
|
|
73
|
+
const SearchViewType = ({ viewType, onChangeViewType }: SearchViewTypeProps) => {
|
|
74
|
+
const { t } = useTranslation();
|
|
75
|
+
const onClickHandler = (viewTypeClicked: SearchViewTypeProps['viewType']) => {
|
|
76
|
+
if (viewTypeClicked !== viewType) {
|
|
77
|
+
onChangeViewType(viewTypeClicked);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return (
|
|
81
|
+
<Wrapper>
|
|
82
|
+
<ButtonContainer>
|
|
83
|
+
<ButtonElement
|
|
84
|
+
stripped
|
|
85
|
+
type="button"
|
|
86
|
+
size="normal"
|
|
87
|
+
title={t('searchPage.resultType.gridView')}
|
|
88
|
+
onClick={() => onClickHandler('grid')}>
|
|
89
|
+
<ButtonContent selected={viewType === 'grid'}>
|
|
90
|
+
<Grid aria-hidden title="" />
|
|
91
|
+
</ButtonContent>
|
|
92
|
+
</ButtonElement>
|
|
93
|
+
</ButtonContainer>
|
|
94
|
+
<ButtonContainer>
|
|
95
|
+
<ButtonElement
|
|
96
|
+
stripped
|
|
97
|
+
size="normal"
|
|
98
|
+
title={t('searchPage.resultType.listView')}
|
|
99
|
+
onClick={() => onClickHandler('list')}>
|
|
100
|
+
<ButtonContent selected={viewType === 'list'}>
|
|
101
|
+
<ListCircle aria-hidden title="" />
|
|
102
|
+
</ButtonContent>
|
|
103
|
+
</ButtonElement>
|
|
104
|
+
</ButtonContainer>
|
|
105
|
+
</Wrapper>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default SearchViewType;
|