@mirrormedia/lilith-draft-editor 1.0.0-beta

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 (105) hide show
  1. package/lib/draft-js/block-renderer/background-image-block.tsx +113 -0
  2. package/lib/draft-js/block-renderer/background-video-block.tsx +120 -0
  3. package/lib/draft-js/block-renderer/color-box-block.tsx +85 -0
  4. package/lib/draft-js/block-renderer/divider-block.tsx +12 -0
  5. package/lib/draft-js/block-renderer/embedded-code-block.tsx +65 -0
  6. package/lib/draft-js/block-renderer/image-block.tsx +41 -0
  7. package/lib/draft-js/block-renderer/info-box-block.tsx +85 -0
  8. package/lib/draft-js/block-renderer/media-block.tsx +36 -0
  9. package/lib/draft-js/block-renderer/related-post-block.tsx +47 -0
  10. package/lib/draft-js/block-renderer/side-index-block.tsx +113 -0
  11. package/lib/draft-js/block-renderer/slideshow-block.tsx +62 -0
  12. package/lib/draft-js/block-renderer/table-block.tsx +488 -0
  13. package/lib/draft-js/buttons/annotation.tsx +113 -0
  14. package/lib/draft-js/buttons/background-color.tsx +125 -0
  15. package/lib/draft-js/buttons/background-image.tsx +276 -0
  16. package/lib/draft-js/buttons/background-video.tsx +275 -0
  17. package/lib/draft-js/buttons/color-box.tsx +207 -0
  18. package/lib/draft-js/buttons/divider.tsx +56 -0
  19. package/lib/draft-js/buttons/embedded-code.tsx +126 -0
  20. package/lib/draft-js/buttons/enlarge.tsx +11 -0
  21. package/lib/draft-js/buttons/font-color.tsx +113 -0
  22. package/lib/draft-js/buttons/image.tsx +71 -0
  23. package/lib/draft-js/buttons/info-box.tsx +170 -0
  24. package/lib/draft-js/buttons/link.tsx +103 -0
  25. package/lib/draft-js/buttons/media.tsx +120 -0
  26. package/lib/draft-js/buttons/related-post.tsx +81 -0
  27. package/lib/draft-js/buttons/selector/align-selector.tsx +65 -0
  28. package/lib/draft-js/buttons/selector/image-selector.tsx +485 -0
  29. package/lib/draft-js/buttons/selector/pagination.tsx +83 -0
  30. package/lib/draft-js/buttons/selector/post-selector.tsx +367 -0
  31. package/lib/draft-js/buttons/selector/search-box.tsx +39 -0
  32. package/lib/draft-js/buttons/selector/video-selector.tsx +312 -0
  33. package/lib/draft-js/buttons/side-index.tsx +257 -0
  34. package/lib/draft-js/buttons/slideshow.tsx +81 -0
  35. package/lib/draft-js/buttons/table.tsx +63 -0
  36. package/lib/draft-js/buttons/text-align.tsx +88 -0
  37. package/lib/draft-js/editor/basic-editor.tsx +384 -0
  38. package/lib/draft-js/editor/block-redender-fn.tsx +77 -0
  39. package/lib/draft-js/editor/draft-converter/api-data-instance.js +58 -0
  40. package/lib/draft-js/editor/draft-converter/atomic-block-processor.js +233 -0
  41. package/lib/draft-js/editor/draft-converter/entities.js +76 -0
  42. package/lib/draft-js/editor/draft-converter/index.js +201 -0
  43. package/lib/draft-js/editor/draft-converter/inline-styles-processor.js +238 -0
  44. package/lib/draft-js/editor/entity-decorator.tsx +7 -0
  45. package/lib/draft-js/editor/modifier.tsx +71 -0
  46. package/lib/draft-js/entity-decorator/annotation-decorator.tsx +81 -0
  47. package/lib/draft-js/entity-decorator/link-decorator.tsx +27 -0
  48. package/lib/index.js +31 -0
  49. package/lib/website/mirrormedia/custom/block-renderer/background-image-block.tsx +128 -0
  50. package/lib/website/mirrormedia/custom/block-renderer/background-video-block.tsx +135 -0
  51. package/lib/website/mirrormedia/custom/block-renderer/color-box-block.tsx +98 -0
  52. package/lib/website/mirrormedia/custom/block-renderer/divider-block.tsx +12 -0
  53. package/lib/website/mirrormedia/custom/block-renderer/embedded-code-block.tsx +65 -0
  54. package/lib/website/mirrormedia/custom/block-renderer/image-block.tsx +41 -0
  55. package/lib/website/mirrormedia/custom/block-renderer/info-box-block.tsx +98 -0
  56. package/lib/website/mirrormedia/custom/block-renderer/media-block.tsx +36 -0
  57. package/lib/website/mirrormedia/custom/block-renderer/related-post-block.tsx +47 -0
  58. package/lib/website/mirrormedia/custom/block-renderer/side-index-block.tsx +125 -0
  59. package/lib/website/mirrormedia/custom/block-renderer/slideshow-block.tsx +62 -0
  60. package/lib/website/mirrormedia/custom/block-renderer/table-block.tsx +537 -0
  61. package/lib/website/mirrormedia/custom/entity-decorator/annotation-decorator.tsx +81 -0
  62. package/lib/website/mirrormedia/custom/entity-decorator/link-decorator.tsx +27 -0
  63. package/lib/website/mirrormedia/custom/selector/align-selector.tsx +65 -0
  64. package/lib/website/mirrormedia/custom/selector/image-selector.tsx +485 -0
  65. package/lib/website/mirrormedia/custom/selector/pagination.tsx +83 -0
  66. package/lib/website/mirrormedia/custom/selector/post-selector.tsx +367 -0
  67. package/lib/website/mirrormedia/custom/selector/search-box.tsx +39 -0
  68. package/lib/website/mirrormedia/custom/selector/video-selector.tsx +310 -0
  69. package/lib/website/mirrormedia/draft-editor/block-redender-fn.tsx +77 -0
  70. package/lib/website/mirrormedia/draft-editor/entity-decorator.tsx +7 -0
  71. package/lib/website/mirrormedia/draft-editor/index.tsx +909 -0
  72. package/lib/website/mirrormedia/draft-renderer/block-redender-fn.tsx +77 -0
  73. package/lib/website/mirrormedia/draft-renderer/entity-decorator.tsx +7 -0
  74. package/lib/website/mirrormedia/draft-renderer/index-deprecated.tsx +43 -0
  75. package/lib/website/mirrormedia/draft-renderer/index.tsx +150 -0
  76. package/lib/website/mirrormedia/index.js +19 -0
  77. package/lib/website/readr/custom/block-renderer/background-image-block.tsx +128 -0
  78. package/lib/website/readr/custom/block-renderer/background-video-block.tsx +135 -0
  79. package/lib/website/readr/custom/block-renderer/color-box-block.tsx +98 -0
  80. package/lib/website/readr/custom/block-renderer/divider-block.tsx +12 -0
  81. package/lib/website/readr/custom/block-renderer/embedded-code-block.tsx +65 -0
  82. package/lib/website/readr/custom/block-renderer/image-block.tsx +41 -0
  83. package/lib/website/readr/custom/block-renderer/info-box-block.tsx +98 -0
  84. package/lib/website/readr/custom/block-renderer/media-block.tsx +36 -0
  85. package/lib/website/readr/custom/block-renderer/related-post-block.tsx +47 -0
  86. package/lib/website/readr/custom/block-renderer/side-index-block.tsx +125 -0
  87. package/lib/website/readr/custom/block-renderer/slideshow-block.tsx +62 -0
  88. package/lib/website/readr/custom/block-renderer/table-block.tsx +537 -0
  89. package/lib/website/readr/custom/entity-decorator/annotation-decorator.tsx +81 -0
  90. package/lib/website/readr/custom/entity-decorator/link-decorator.tsx +27 -0
  91. package/lib/website/readr/custom/selector/align-selector.tsx +65 -0
  92. package/lib/website/readr/custom/selector/image-selector.tsx +485 -0
  93. package/lib/website/readr/custom/selector/pagination.tsx +83 -0
  94. package/lib/website/readr/custom/selector/post-selector.tsx +367 -0
  95. package/lib/website/readr/custom/selector/search-box.tsx +39 -0
  96. package/lib/website/readr/custom/selector/video-selector.tsx +310 -0
  97. package/lib/website/readr/draft-editor/block-redender-fn.tsx +77 -0
  98. package/lib/website/readr/draft-editor/entity-decorator.tsx +7 -0
  99. package/lib/website/readr/draft-editor/index.tsx +909 -0
  100. package/lib/website/readr/draft-renderer/block-redender-fn.tsx +77 -0
  101. package/lib/website/readr/draft-renderer/entity-decorator.tsx +7 -0
  102. package/lib/website/readr/draft-renderer/index-deprecated.tsx +43 -0
  103. package/lib/website/readr/draft-renderer/index.tsx +150 -0
  104. package/lib/website/readr/index.js +19 -0
  105. package/package.json +39 -0
@@ -0,0 +1,367 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import styled from 'styled-components'
3
+ import { Drawer, DrawerController } from '@keystone-ui/modals'
4
+ import { gql, useLazyQuery } from '@keystone-6/core/admin-ui/apollo'
5
+ import { ImageEntity } from './image-selector'
6
+ import { SearchBox, SearchBoxOnChangeFn } from './search-box'
7
+ import { Pagination } from './pagination'
8
+
9
+ const postsQuery = gql`
10
+ query Posts($searchText: String!, $take: Int, $skip: Int) {
11
+ postsCount(where: { name: { contains: $searchText } })
12
+ posts(
13
+ where: { name: { contains: $searchText } }
14
+ take: $take
15
+ skip: $skip
16
+ ) {
17
+ id
18
+ slug
19
+ name
20
+ subtitle
21
+ heroImage {
22
+ id
23
+ name
24
+ imageFile {
25
+ url
26
+ }
27
+ resized {
28
+ original
29
+ }
30
+ }
31
+ ogImage {
32
+ id
33
+ name
34
+ imageFile {
35
+ url
36
+ }
37
+ resized {
38
+ original
39
+ }
40
+ }
41
+ }
42
+ }
43
+ `
44
+
45
+ const PostSearchBox = styled(SearchBox)`
46
+ margin-top: 10px;
47
+ `
48
+
49
+ const PostSelectionWrapper = styled.div`
50
+ overflow: auto;
51
+ margin-top: 10px;
52
+ `
53
+
54
+ const PostGridsWrapper = styled.div`
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ overflow: auto;
58
+ `
59
+
60
+ const PostGridWrapper = styled.div`
61
+ flex: 0 0 33.3333%;
62
+ cursor: pointer;
63
+ padding: 0 10px 10px;
64
+ `
65
+
66
+ const PostMetaGridsWrapper = styled.div`
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ overflow: auto;
70
+ `
71
+
72
+ const PostMetaGridWrapper = styled.div`
73
+ flex: 0 0 33.3333%;
74
+ cursor: pointer;
75
+ padding: 0 10px 10px;
76
+ `
77
+
78
+ const Post = styled.div`
79
+ width: 100%;
80
+ `
81
+
82
+ const SeparationLine = styled.div`
83
+ border: #e1e5e9 1px solid;
84
+ margin-top: 10px;
85
+ margin-bottom: 10px;
86
+ `
87
+
88
+ const ErrorHint = styled.span`
89
+ color: red;
90
+ `
91
+
92
+ const PostSelected = styled.div`
93
+ height: 1.4rem;
94
+ `
95
+
96
+ const PostImage = styled.img`
97
+ display: block;
98
+ width: 100%;
99
+ aspect-ratio: 2;
100
+ object-fit: cover;
101
+ `
102
+
103
+ const PostTitle = styled.div`
104
+ padding: 0 5px;
105
+ `
106
+
107
+ const ErrorWrapper = styled.div`
108
+ & * {
109
+ margin: 0;
110
+ }
111
+ `
112
+
113
+ type ID = string
114
+
115
+ export type PostEntity = {
116
+ id: ID
117
+ name: string
118
+ slug: string
119
+ heroImage: ImageEntity
120
+ ogImage: ImageEntity
121
+ }
122
+
123
+ export type PostEntityWithMeta = {
124
+ post: PostEntity
125
+ }
126
+
127
+ type PostEntityOnSelectFn = (param: PostEntity) => void
128
+
129
+ function PostGrids(props: {
130
+ posts: PostEntity[]
131
+ selected: PostEntity[]
132
+ onSelect: PostEntityOnSelectFn
133
+ }): React.ReactElement {
134
+ const { posts, selected, onSelect } = props
135
+
136
+ return (
137
+ <PostGridsWrapper>
138
+ {posts.map((post) => {
139
+ return (
140
+ <PostGrid
141
+ key={post.id}
142
+ isSelected={selected?.includes(post)}
143
+ onSelect={() => onSelect(post)}
144
+ post={post}
145
+ />
146
+ )
147
+ })}
148
+ </PostGridsWrapper>
149
+ )
150
+ }
151
+
152
+ function PostGrid(props: {
153
+ post: PostEntity
154
+ isSelected: boolean
155
+ onSelect: PostEntityOnSelectFn
156
+ }) {
157
+ const { post, onSelect, isSelected } = props
158
+ return (
159
+ <PostGridWrapper key={post?.id} onClick={() => onSelect(post)}>
160
+ <PostSelected>
161
+ {isSelected ? <i className="fas fa-check-circle"></i> : null}
162
+ </PostSelected>
163
+ <Post>
164
+ <PostImage
165
+ src={post.heroImage?.resized?.original}
166
+ onError={(e) =>
167
+ (e.currentTarget.src = post.heroImage?.imageFile?.url)
168
+ }
169
+ />
170
+ <PostTitle>{post.name}</PostTitle>
171
+ </Post>
172
+ </PostGridWrapper>
173
+ )
174
+ }
175
+
176
+ function PostMetaGrids(props: { postMetas: PostEntityWithMeta[] }) {
177
+ const { postMetas } = props
178
+ return (
179
+ <PostMetaGridsWrapper>
180
+ {postMetas.map((postMetas) => (
181
+ <PostMetaGrid key={postMetas?.post?.id} postMeta={postMetas} />
182
+ ))}
183
+ </PostMetaGridsWrapper>
184
+ )
185
+ }
186
+
187
+ function PostMetaGrid(props: {
188
+ postMeta: PostEntityWithMeta
189
+ }): React.ReactElement {
190
+ const { postMeta } = props
191
+ const { post } = postMeta
192
+
193
+ return (
194
+ <PostMetaGridWrapper>
195
+ <Post>
196
+ <PostImage
197
+ src={post?.heroImage?.resized?.original}
198
+ onError={(e) =>
199
+ (e.currentTarget.src = post?.heroImage?.imageFile?.url)
200
+ }
201
+ />
202
+ <PostTitle>{post?.name}</PostTitle>
203
+ </Post>
204
+ </PostMetaGridWrapper>
205
+ )
206
+ }
207
+
208
+ type PostSelectorOnChangeFn = (params: PostEntityWithMeta[]) => void
209
+
210
+ export function PostSelector(props: {
211
+ onChange: PostSelectorOnChangeFn
212
+ enableMultiSelect?: boolean
213
+ minSelectCount?: number
214
+ maxSelectCount?: number
215
+ }) {
216
+ const [
217
+ queryPosts,
218
+ { loading, error, data: { posts = [], postsCount = 0 } = {} },
219
+ ] = useLazyQuery(postsQuery, { fetchPolicy: 'no-cache' })
220
+ const [currentPage, setCurrentPage] = useState(0) // page starts with 1, 0 is used to detect initialization
221
+ const [searchText, setSearchText] = useState('')
222
+ const [selected, setSelected] = useState<PostEntityWithMeta[]>([])
223
+ const [showErrorHint, setShowErrorHint] = useState(false)
224
+
225
+ const pageSize = 6
226
+
227
+ const {
228
+ onChange,
229
+ enableMultiSelect = false,
230
+ minSelectCount = 1,
231
+ maxSelectCount = 3,
232
+ } = props
233
+
234
+ const onSave = () => {
235
+ if (
236
+ enableMultiSelect &&
237
+ minSelectCount &&
238
+ selected.length < minSelectCount
239
+ ) {
240
+ setShowErrorHint(true)
241
+ return
242
+ }
243
+
244
+ onChange(selected)
245
+ }
246
+
247
+ const onCancel = () => {
248
+ onChange([])
249
+ }
250
+
251
+ const onSearchBoxChange: SearchBoxOnChangeFn = async (searchInput) => {
252
+ setSearchText(searchInput)
253
+ setCurrentPage(1)
254
+ }
255
+
256
+ const onPostsGridSelect: PostEntityOnSelectFn = (postEntity) => {
257
+ setSelected((selected) => {
258
+ const filterdSelected = selected.filter(
259
+ (ele) => ele.post?.id !== postEntity.id
260
+ )
261
+
262
+ // deselect the post
263
+ if (filterdSelected.length !== selected.length) {
264
+ return filterdSelected
265
+ }
266
+
267
+ // add new selected one and check shrink the array if there is a limit
268
+ if (enableMultiSelect) {
269
+ let newSelected = selected.concat([{ post: postEntity }])
270
+
271
+ if (maxSelectCount && newSelected.length >= maxSelectCount) {
272
+ newSelected = newSelected.slice(-maxSelectCount)
273
+ }
274
+
275
+ return newSelected
276
+ }
277
+
278
+ // single select
279
+ return [{ post: postEntity }]
280
+ })
281
+ }
282
+
283
+ const selectedPosts = selected.map((ele: PostEntityWithMeta) => {
284
+ return ele.post
285
+ })
286
+
287
+ useEffect(() => {
288
+ if (currentPage !== 0) {
289
+ queryPosts({
290
+ variables: {
291
+ searchText: searchText,
292
+ skip: (currentPage - 1) * pageSize,
293
+ take: pageSize,
294
+ },
295
+ })
296
+ }
297
+ }, [currentPage, searchText])
298
+
299
+ let searchResult = (
300
+ <React.Fragment>
301
+ <PostGrids
302
+ posts={posts}
303
+ selected={selectedPosts}
304
+ onSelect={onPostsGridSelect}
305
+ />
306
+ <Pagination
307
+ currentPage={currentPage}
308
+ total={postsCount}
309
+ pageSize={pageSize}
310
+ onChange={(pageIndex) => {
311
+ setCurrentPage(pageIndex)
312
+ }}
313
+ />
314
+ </React.Fragment>
315
+ )
316
+ if (loading) {
317
+ searchResult = <p>searching...</p>
318
+ }
319
+ if (error) {
320
+ searchResult = (
321
+ <ErrorWrapper>
322
+ <h3>Errors occurs in the `posts` query</h3>
323
+ <div>
324
+ <br />
325
+ <b>Message:</b>
326
+ <div>{error.message}</div>
327
+ <br />
328
+ <b>Stack:</b>
329
+ <div>{error.stack}</div>
330
+ <br />
331
+ <b>Query:</b>
332
+ <pre>{postsQuery.loc.source.body}</pre>
333
+ </div>
334
+ </ErrorWrapper>
335
+ )
336
+ }
337
+
338
+ return (
339
+ <DrawerController isOpen={true}>
340
+ <Drawer
341
+ title="Select post"
342
+ actions={{
343
+ cancel: {
344
+ label: 'Cancel',
345
+ action: onCancel,
346
+ },
347
+ confirm: {
348
+ label: 'Confirm',
349
+ action: onSave,
350
+ },
351
+ }}
352
+ >
353
+ <div>
354
+ <PostSearchBox onChange={onSearchBoxChange} />
355
+ <PostSelectionWrapper>
356
+ <div>{searchResult} </div>
357
+ {!!selected.length && <SeparationLine />}
358
+ <PostMetaGrids postMetas={selected} />
359
+ {showErrorHint && (
360
+ <ErrorHint>請至少選擇{minSelectCount}則文章</ErrorHint>
361
+ )}
362
+ </PostSelectionWrapper>
363
+ </div>
364
+ </Drawer>
365
+ </DrawerController>
366
+ )
367
+ }
@@ -0,0 +1,39 @@
1
+ import React, { useState } from 'react'
2
+ import styled from 'styled-components'
3
+ import { Button } from '@keystone-ui/button'
4
+ import { TextInput } from '@keystone-ui/fields'
5
+
6
+ const SearchBoxWrapper = styled.div`
7
+ display: flex;
8
+ `
9
+
10
+ export type SearchBoxOnChangeFn = (param: string) => void
11
+
12
+ export function SearchBox(props: {
13
+ onChange: SearchBoxOnChangeFn
14
+ className: string
15
+ }): React.ReactElement {
16
+ const { onChange, className } = props
17
+ const [searchInput, setSearchInput] = useState('')
18
+
19
+ return (
20
+ <SearchBoxWrapper className={className}>
21
+ <TextInput
22
+ type="text"
23
+ placeholder="請輸入關鍵字搜尋"
24
+ value={searchInput}
25
+ onChange={(e) => {
26
+ setSearchInput(e.target.value)
27
+ }}
28
+ ></TextInput>
29
+
30
+ <Button
31
+ onClick={() => {
32
+ onChange(searchInput)
33
+ }}
34
+ >
35
+ Search
36
+ </Button>
37
+ </SearchBoxWrapper>
38
+ )
39
+ }
@@ -0,0 +1,310 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import styled from 'styled-components'
3
+ import { Drawer, DrawerController } from '@keystone-ui/modals'
4
+ import { gql, useLazyQuery } from '@keystone-6/core/admin-ui/apollo'
5
+ import { ImageEntity } from './image-selector'
6
+ import { SearchBox, SearchBoxOnChangeFn } from './search-box'
7
+ import { Pagination } from './pagination'
8
+
9
+ const videosQuery = gql`
10
+ query Videos($searchText: String!, $take: Int, $skip: Int) {
11
+ videosCount(where: { name: { contains: $searchText } })
12
+ videos(
13
+ where: { name: { contains: $searchText } }
14
+ take: $take
15
+ skip: $skip
16
+ ) {
17
+ id
18
+ name
19
+ url
20
+ file {
21
+ filename
22
+ filesize
23
+ url
24
+ }
25
+ coverPhoto {
26
+ id
27
+ name
28
+ imageFile {
29
+ url
30
+ }
31
+ resized {
32
+ original
33
+ }
34
+ }
35
+ }
36
+ }
37
+ `
38
+
39
+ const VideoSearchBox = styled(SearchBox)`
40
+ margin-top: 10px;
41
+ `
42
+
43
+ const VideoSelectionWrapper = styled.div`
44
+ overflow: auto;
45
+ margin-top: 10px;
46
+ `
47
+
48
+ const VideoGridsWrapper = styled.div`
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ overflow: auto;
52
+ `
53
+
54
+ const VideoGridWrapper = styled.div`
55
+ flex: 0 0 33.3333%;
56
+ cursor: pointer;
57
+ padding: 0 10px 10px;
58
+ `
59
+
60
+ const VideoMetaGridsWrapper = styled.div`
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ overflow: auto;
64
+ `
65
+
66
+ const VideoMetaGridWrapper = styled.div`
67
+ flex: 0 0 33.3333%;
68
+ cursor: pointer;
69
+ padding: 0 10px 10px;
70
+ `
71
+
72
+ const Video = styled.video`
73
+ display: block;
74
+ width: 100%;
75
+ aspect-ratio: 2;
76
+ object-fit: cover;
77
+ `
78
+
79
+ const SeparationLine = styled.div`
80
+ border: #e1e5e9 1px solid;
81
+ margin-top: 10px;
82
+ margin-bottom: 10px;
83
+ `
84
+
85
+ const VideoSelected = styled.div`
86
+ height: 1.4rem;
87
+ `
88
+
89
+ const ErrorWrapper = styled.div`
90
+ & * {
91
+ margin: 0;
92
+ }
93
+ `
94
+
95
+ type ID = string
96
+
97
+ export type VideoEntity = {
98
+ id: ID
99
+ name?: string
100
+ url: string
101
+ youtubeUrl?: string
102
+ file: {
103
+ filename?: string
104
+ filesize: number
105
+ url: string
106
+ }
107
+ coverPhoto: ImageEntity
108
+ }
109
+
110
+ export type VideoEntityWithMeta = {
111
+ video: VideoEntity
112
+ }
113
+
114
+ type VideoEntityOnSelectFn = (param: VideoEntity) => void
115
+
116
+ function VideosGrids(props: {
117
+ videos: VideoEntity[]
118
+ selected: VideoEntity[]
119
+ onSelect: VideoEntityOnSelectFn
120
+ }): React.ReactElement {
121
+ const { videos, selected, onSelect } = props
122
+
123
+ return (
124
+ <VideoGridsWrapper>
125
+ {videos.map((video) => {
126
+ return (
127
+ <VideoGrid
128
+ key={video.id}
129
+ isSelected={selected?.includes(video)}
130
+ onSelect={() => onSelect(video)}
131
+ video={video}
132
+ />
133
+ )
134
+ })}
135
+ </VideoGridsWrapper>
136
+ )
137
+ }
138
+
139
+ function VideoGrid(props: {
140
+ video: VideoEntity
141
+ isSelected: boolean
142
+ onSelect: VideoEntityOnSelectFn
143
+ }) {
144
+ const { video, onSelect, isSelected } = props
145
+ return (
146
+ <VideoGridWrapper key={video?.id} onClick={() => onSelect(video)}>
147
+ <VideoSelected>
148
+ {isSelected ? <i className="fas fa-check-circle"></i> : null}
149
+ </VideoSelected>
150
+ <Video muted loop>
151
+ <source src={video?.url} />
152
+ <source src={video?.file?.url} />
153
+ </Video>
154
+ </VideoGridWrapper>
155
+ )
156
+ }
157
+
158
+ function VideoMetaGrids(props: { videoMetas: VideoEntityWithMeta[] }) {
159
+ const { videoMetas } = props
160
+ return (
161
+ <VideoMetaGridsWrapper>
162
+ {videoMetas.map((videoMeta) => (
163
+ <VideoMetaGrid key={videoMeta?.video?.id} videoMeta={videoMeta} />
164
+ ))}
165
+ </VideoMetaGridsWrapper>
166
+ )
167
+ }
168
+
169
+ function VideoMetaGrid(props: {
170
+ videoMeta: VideoEntityWithMeta
171
+ }): React.ReactElement {
172
+ const { videoMeta } = props
173
+ const { video } = videoMeta
174
+
175
+ return (
176
+ <VideoMetaGridWrapper>
177
+ <Video muted autoPlay loop>
178
+ <source src={video?.url} />
179
+ <source src={video?.file?.url} />
180
+ </Video>
181
+ </VideoMetaGridWrapper>
182
+ )
183
+ }
184
+
185
+ type VideoSelectorOnChangeFn = (params: VideoEntityWithMeta[]) => void
186
+
187
+ export function VideoSelector(props: { onChange: VideoSelectorOnChangeFn }) {
188
+ const [
189
+ queryVideos,
190
+ { loading, error, data: { videos = [], videosCount = 0 } = {} },
191
+ ] = useLazyQuery(videosQuery, { fetchPolicy: 'no-cache' })
192
+ const [currentPage, setCurrentPage] = useState(0) // page starts with 1, 0 is used to detect initialization
193
+ const [searchText, setSearchText] = useState('')
194
+ const [selected, setSelected] = useState<VideoEntityWithMeta[]>([])
195
+
196
+ const pageSize = 6
197
+
198
+ const { onChange } = props
199
+
200
+ const onSave = () => {
201
+ onChange(selected)
202
+ }
203
+
204
+ const onCancel = () => {
205
+ onChange([])
206
+ }
207
+
208
+ const onSearchBoxChange: SearchBoxOnChangeFn = async (searchInput) => {
209
+ setSearchText(searchInput)
210
+ setCurrentPage(1)
211
+ }
212
+
213
+ const onVideosGridSelect: VideoEntityOnSelectFn = (videoEntity) => {
214
+ setSelected((selected) => {
215
+ const filterdSelected = selected.filter(
216
+ (ele) => ele.video?.id !== videoEntity.id
217
+ )
218
+
219
+ // deselect the video
220
+ if (filterdSelected.length !== selected.length) {
221
+ return filterdSelected
222
+ }
223
+
224
+ // single select
225
+ return [{ video: videoEntity }]
226
+ })
227
+ }
228
+
229
+ const selectedVideos = selected.map((ele: VideoEntityWithMeta) => {
230
+ return ele.video
231
+ })
232
+
233
+ useEffect(() => {
234
+ if (currentPage !== 0) {
235
+ queryVideos({
236
+ variables: {
237
+ searchText: searchText,
238
+ skip: (currentPage - 1) * pageSize,
239
+ take: pageSize,
240
+ },
241
+ })
242
+ }
243
+ }, [currentPage, searchText])
244
+
245
+ let searchResult = (
246
+ <React.Fragment>
247
+ <VideosGrids
248
+ videos={videos}
249
+ selected={selectedVideos}
250
+ onSelect={onVideosGridSelect}
251
+ />
252
+ <Pagination
253
+ currentPage={currentPage}
254
+ total={videosCount}
255
+ pageSize={pageSize}
256
+ onChange={(pageIndex) => {
257
+ setCurrentPage(pageIndex)
258
+ }}
259
+ />
260
+ </React.Fragment>
261
+ )
262
+ if (loading) {
263
+ searchResult = <p>searching...</p>
264
+ }
265
+ if (error) {
266
+ searchResult = (
267
+ <ErrorWrapper>
268
+ <h3>Errors occurs in the `videos` query</h3>
269
+ <div>
270
+ <br />
271
+ <b>Message:</b>
272
+ <div>{error.message}</div>
273
+ <br />
274
+ <b>Stack:</b>
275
+ <div>{error.stack}</div>
276
+ <br />
277
+ <b>Query:</b>
278
+ <pre>{videosQuery.loc.source.body}</pre>
279
+ </div>
280
+ </ErrorWrapper>
281
+ )
282
+ }
283
+
284
+ return (
285
+ <DrawerController isOpen={true}>
286
+ <Drawer
287
+ title="Select video"
288
+ actions={{
289
+ cancel: {
290
+ label: 'Cancel',
291
+ action: onCancel,
292
+ },
293
+ confirm: {
294
+ label: 'Confirm',
295
+ action: onSave,
296
+ },
297
+ }}
298
+ >
299
+ <div>
300
+ <VideoSearchBox onChange={onSearchBoxChange} />
301
+ <VideoSelectionWrapper>
302
+ <div>{searchResult}</div>
303
+ {!!selected.length && <SeparationLine />}
304
+ <VideoMetaGrids videoMetas={selected} />
305
+ </VideoSelectionWrapper>
306
+ </div>
307
+ </Drawer>
308
+ </DrawerController>
309
+ )
310
+ }