@ndla/ui 4.3.0 → 5.0.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.
Files changed (177) hide show
  1. package/README.md +11 -11
  2. package/es/Article/Article.js +3 -7
  3. package/es/Article/ArticleByline.js +4 -4
  4. package/es/Article/ArticleNotions.js +37 -92
  5. package/es/ContentTypeBadge/ContentTypeBadge.js +10 -1
  6. package/es/FactBox/FactBox.js +36 -11
  7. package/es/Filter/FilterButtons.js +18 -17
  8. package/es/Filter/FilterCarousel.js +198 -0
  9. package/es/Masthead/Masthead.js +24 -2
  10. package/es/Notion/ConceptNotion.js +57 -0
  11. package/es/Notion/FigureNotion.js +82 -0
  12. package/es/Notion/Notion.js +122 -48
  13. package/es/Notion/NotionImage.js +47 -0
  14. package/es/Notion/NotionVisualElement.js +38 -0
  15. package/es/Notion/index.js +2 -1
  16. package/es/SearchTypeResult/ActiveFilterContent.js +17 -2
  17. package/es/SearchTypeResult/ActiveFilters.js +64 -50
  18. package/es/SearchTypeResult/PopupFilter.js +28 -125
  19. package/es/SearchTypeResult/ResultNavigation.js +41 -14
  20. package/es/SearchTypeResult/SearchFieldHeader.js +20 -41
  21. package/es/SearchTypeResult/SearchFilterContent.js +61 -0
  22. package/es/SearchTypeResult/SearchHeader.js +51 -28
  23. package/es/SearchTypeResult/SearchItem.js +64 -190
  24. package/es/SearchTypeResult/SearchItemList.js +132 -0
  25. package/es/SearchTypeResult/SearchItems.js +17 -13
  26. package/es/SearchTypeResult/SearchNotionItem.js +13 -13
  27. package/es/SearchTypeResult/SearchNotionsResult.js +16 -21
  28. package/es/SearchTypeResult/SearchTypeHeader.js +43 -26
  29. package/es/SearchTypeResult/SearchTypeResult.js +7 -5
  30. package/es/SearchTypeResult/SearchViewType.js +93 -0
  31. package/es/SearchTypeResult/components/ItemContexts.js +125 -0
  32. package/es/SearchTypeResult/components/ItemResourceHeader.js +87 -0
  33. package/es/SearchTypeResult/components/ItemTopicHeader.js +56 -0
  34. package/es/SearchTypeResult/components/SubjectFilters.js +177 -0
  35. package/es/SearchTypeResult/index.js +2 -1
  36. package/es/all.css +1 -1
  37. package/es/index-javascript.js +1 -2
  38. package/es/index.js +3 -2
  39. package/es/locale/messages-en.js +21 -6
  40. package/es/locale/messages-nb.js +21 -6
  41. package/es/locale/messages-nn.js +22 -7
  42. package/es/model/ContentType.js +2 -1
  43. package/es/shapes.js +1 -1
  44. package/lib/Article/Article.d.ts +4 -5
  45. package/lib/Article/Article.js +3 -7
  46. package/lib/Article/ArticleByline.js +4 -4
  47. package/lib/Article/ArticleNotions.d.ts +3 -8
  48. package/lib/Article/ArticleNotions.js +41 -90
  49. package/lib/ContentTypeBadge/ContentTypeBadge.d.ts +1 -0
  50. package/lib/ContentTypeBadge/ContentTypeBadge.js +14 -2
  51. package/lib/FactBox/FactBox.js +41 -8
  52. package/lib/Filter/FilterButtons.d.ts +3 -10
  53. package/lib/Filter/FilterButtons.js +19 -17
  54. package/lib/Filter/FilterCarousel.d.ts +13 -0
  55. package/lib/Filter/FilterCarousel.js +207 -0
  56. package/lib/Masthead/Masthead.js +30 -2
  57. package/lib/Notion/ConceptNotion.d.ts +25 -0
  58. package/lib/Notion/ConceptNotion.js +79 -0
  59. package/lib/Notion/FigureNotion.d.ts +23 -0
  60. package/lib/Notion/FigureNotion.js +97 -0
  61. package/lib/Notion/Notion.d.ts +24 -11
  62. package/lib/Notion/Notion.js +120 -48
  63. package/lib/Notion/NotionImage.d.ts +15 -0
  64. package/lib/Notion/NotionImage.js +63 -0
  65. package/lib/Notion/NotionVisualElement.d.ts +22 -0
  66. package/lib/Notion/NotionVisualElement.js +51 -0
  67. package/lib/Notion/index.d.ts +1 -0
  68. package/lib/Notion/index.js +8 -0
  69. package/lib/SearchTypeResult/ActiveFilterContent.js +16 -9
  70. package/lib/SearchTypeResult/ActiveFilters.d.ts +2 -1
  71. package/lib/SearchTypeResult/ActiveFilters.js +65 -50
  72. package/lib/SearchTypeResult/PopupFilter.d.ts +13 -19
  73. package/lib/SearchTypeResult/PopupFilter.js +27 -123
  74. package/lib/SearchTypeResult/ResultNavigation.d.ts +2 -2
  75. package/lib/SearchTypeResult/ResultNavigation.js +38 -14
  76. package/lib/SearchTypeResult/SearchFieldHeader.d.ts +3 -8
  77. package/lib/SearchTypeResult/SearchFieldHeader.js +18 -39
  78. package/lib/SearchTypeResult/SearchFilterContent.d.ts +16 -0
  79. package/lib/SearchTypeResult/SearchFilterContent.js +67 -0
  80. package/lib/SearchTypeResult/SearchHeader.d.ts +3 -7
  81. package/lib/SearchTypeResult/SearchHeader.js +59 -30
  82. package/lib/SearchTypeResult/SearchItem.d.ts +8 -12
  83. package/lib/SearchTypeResult/SearchItem.js +64 -187
  84. package/lib/SearchTypeResult/SearchItemList.d.ts +10 -0
  85. package/lib/SearchTypeResult/SearchItemList.js +139 -0
  86. package/lib/SearchTypeResult/SearchItems.d.ts +4 -3
  87. package/lib/SearchTypeResult/SearchItems.js +18 -13
  88. package/lib/SearchTypeResult/SearchNotionItem.js +13 -13
  89. package/lib/SearchTypeResult/SearchNotionsResult.d.ts +2 -4
  90. package/lib/SearchTypeResult/SearchNotionsResult.js +23 -23
  91. package/lib/SearchTypeResult/SearchTypeHeader.d.ts +2 -2
  92. package/lib/SearchTypeResult/SearchTypeHeader.js +40 -25
  93. package/lib/SearchTypeResult/SearchTypeResult.d.ts +7 -6
  94. package/lib/SearchTypeResult/SearchTypeResult.js +7 -5
  95. package/lib/SearchTypeResult/SearchViewType.d.ts +13 -0
  96. package/lib/SearchTypeResult/SearchViewType.js +99 -0
  97. package/lib/SearchTypeResult/components/ItemContexts.d.ts +19 -0
  98. package/lib/SearchTypeResult/components/ItemContexts.js +134 -0
  99. package/lib/SearchTypeResult/components/ItemResourceHeader.d.ts +16 -0
  100. package/lib/SearchTypeResult/components/ItemResourceHeader.js +98 -0
  101. package/lib/SearchTypeResult/components/ItemTopicHeader.d.ts +17 -0
  102. package/lib/SearchTypeResult/components/ItemTopicHeader.js +67 -0
  103. package/lib/SearchTypeResult/components/SubjectFilters.d.ts +32 -0
  104. package/lib/SearchTypeResult/components/SubjectFilters.js +192 -0
  105. package/lib/SearchTypeResult/index.d.ts +2 -1
  106. package/lib/SearchTypeResult/index.js +8 -0
  107. package/lib/all.css +1 -1
  108. package/lib/index-javascript.js +0 -20
  109. package/lib/index.d.ts +2 -1
  110. package/lib/index.js +24 -1
  111. package/lib/locale/messages-en.d.ts +16 -1
  112. package/lib/locale/messages-en.js +21 -6
  113. package/lib/locale/messages-nb.d.ts +15 -0
  114. package/lib/locale/messages-nb.js +21 -6
  115. package/lib/locale/messages-nn.d.ts +16 -1
  116. package/lib/locale/messages-nn.js +22 -7
  117. package/lib/model/ContentType.d.ts +1 -0
  118. package/lib/model/ContentType.js +4 -2
  119. package/lib/shapes.js +1 -1
  120. package/lib/types.d.ts +1 -0
  121. package/package.json +14 -15
  122. package/src/Article/Article.tsx +8 -15
  123. package/src/Article/ArticleByline.tsx +1 -1
  124. package/src/Article/ArticleNotions.tsx +13 -33
  125. package/src/ContentTypeBadge/ContentTypeBadge.tsx +8 -0
  126. package/src/ContentTypeBadge/component.content-type-badge.scss +5 -0
  127. package/src/FactBox/FactBox.tsx +22 -15
  128. package/src/Figure/component.figure.scss +1 -1
  129. package/src/Filter/FilterButtons.tsx +14 -15
  130. package/src/Filter/FilterCarousel.tsx +166 -0
  131. package/src/Masthead/Masthead.tsx +42 -18
  132. package/src/Notion/ConceptNotion.tsx +80 -0
  133. package/src/Notion/FigureNotion.tsx +86 -0
  134. package/src/Notion/Notion.tsx +205 -75
  135. package/src/Notion/NotionImage.tsx +51 -0
  136. package/src/Notion/NotionVisualElement.tsx +50 -0
  137. package/src/Notion/index.ts +1 -0
  138. package/src/SearchTypeResult/ActiveFilterContent.tsx +7 -2
  139. package/src/SearchTypeResult/ActiveFilters.tsx +72 -38
  140. package/src/SearchTypeResult/PopupFilter.tsx +73 -146
  141. package/src/SearchTypeResult/ResultNavigation.tsx +54 -16
  142. package/src/SearchTypeResult/SearchFieldHeader.tsx +15 -40
  143. package/src/SearchTypeResult/SearchFilterContent.tsx +63 -0
  144. package/src/SearchTypeResult/SearchHeader.tsx +31 -31
  145. package/src/SearchTypeResult/SearchItem.tsx +145 -233
  146. package/src/SearchTypeResult/SearchItemList.tsx +167 -0
  147. package/src/SearchTypeResult/SearchItems.tsx +26 -19
  148. package/src/SearchTypeResult/SearchNotionItem.tsx +5 -12
  149. package/src/SearchTypeResult/SearchNotionsResult.tsx +29 -22
  150. package/src/SearchTypeResult/SearchTypeHeader.tsx +51 -33
  151. package/src/SearchTypeResult/SearchTypeResult.tsx +13 -12
  152. package/src/SearchTypeResult/SearchViewType.tsx +109 -0
  153. package/src/SearchTypeResult/components/ItemContexts.tsx +138 -0
  154. package/src/SearchTypeResult/components/ItemResourceHeader.tsx +133 -0
  155. package/src/SearchTypeResult/components/ItemTopicHeader.tsx +95 -0
  156. package/src/SearchTypeResult/components/SubjectFilters.tsx +152 -0
  157. package/src/SearchTypeResult/index.ts +9 -1
  158. package/src/index-javascript.js +0 -2
  159. package/src/index.ts +3 -0
  160. package/src/locale/messages-en.ts +19 -4
  161. package/src/locale/messages-nb.ts +19 -4
  162. package/src/locale/messages-nn.ts +20 -5
  163. package/src/model/ContentType.ts +1 -0
  164. package/src/shapes.js +1 -0
  165. package/src/types.ts +1 -0
  166. package/es/Embedded/Facebook.js +0 -19
  167. package/es/Embedded/FacebookPage.js +0 -22
  168. package/es/Embedded/Twitter.js +0 -121
  169. package/es/Embedded/index.js +0 -3
  170. package/lib/Embedded/Facebook.js +0 -32
  171. package/lib/Embedded/FacebookPage.js +0 -35
  172. package/lib/Embedded/Twitter.js +0 -132
  173. package/lib/Embedded/index.js +0 -31
  174. package/src/Embedded/Facebook.jsx +0 -17
  175. package/src/Embedded/FacebookPage.jsx +0 -24
  176. package/src/Embedded/Twitter.jsx +0 -71
  177. package/src/Embedded/index.js +0 -3
@@ -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
+ <>&#8226;</> {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, { SearchItemType } from './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
- itemCount: number;
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
- ${mq.range({ from: breakpoints.tablet })} {
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
- ${mq.range({ from: breakpoints.tabletWide })} {
34
- grid-template-columns: repeat(3, 1fr);
35
- }
37
+
36
38
  ${mq.range({ from: breakpoints.desktop })} {
37
- grid-template-columns: repeat(4, 1fr);
38
- }
39
+ grid-template-columns: repeat(3, 1fr);
40
+ }`}
39
41
  `;
40
42
 
41
43
  type Props = {
42
- items: SearchItemType[];
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
- {type === 'video' && (
152
- <>
153
- <Play />
154
- <ShowMediaButtonText>{t('searchPage.resultType.showVideo')}</ShowMediaButtonText>
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
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.24);
23
- border-radius: 5px;
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.medium} 66px;
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
- margin-bottom: ${spacing.small};
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
- renderMarkdown: (text: ReactNode) => string;
59
+ children: ReactNode;
54
60
  };
55
61
 
56
- const SearchNotionsResult = ({ items, totalCount, onRemove, renderMarkdown, t }: Props & WithTranslation) => (
57
- <Wrapper>
58
- <HeadingWrapper>
59
- <Heading>
60
- {t(`searchPage.resultType.notionsHeading`)}
61
- <HeadingCount>{t(`searchPage.resultType.hits`, { count: totalCount })}</HeadingCount>
62
- </Heading>
63
- <ModalCloseButton onClick={onRemove} />
64
- </HeadingWrapper>
65
- {items.map((item) => (
66
- <SearchNotionItem key={item.id} {...item} renderMarkdown={renderMarkdown} />
67
- ))}
68
- </Wrapper>
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 { WithTranslation, withTranslation } from 'react-i18next';
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
- font-size: 18px;
41
- margin: 2px ${spacing.small};
46
+ ${fonts.sizes('18px', '24px')};
47
+ margin: 2px 0;
42
48
  b {
43
- font-size: 18px;
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
- ${mq.range({ until: breakpoints.tablet })} {
67
- justify-content: flex-start;
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: FilterOptionsType[];
79
- onFilterClick: (id: string) => void;
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
- <HeaderWrapper>
85
- <TypeWrapper>
86
- {type && <ContentTypeBadge type={type} title={t(`contentTypes.${type}`)} background size="large" />}
87
- <SubjectName>
88
- {type && <b>{t(`contentTypes.${type}`)}</b>}{' '}
89
- {totalCount && <Count>{t(`searchPage.resultType.hits`, { count: totalCount })}</Count>}
90
- </SubjectName>
91
- </TypeWrapper>
92
- {filters && (
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="small"
98
- lighter={!option.active}
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
- </HeaderWrapper>
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 { SearchItemType } from './SearchItem';
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: SearchItemType[];
54
- filters: FilterOptionsType[];
55
- onFilterClick: (id: string) => void;
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
- <Overlay>
77
- <Spinner />
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 && 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;