1
- import React , { useState } from "react"
1
+ import React , { useEffect , useMemo , useState } from "react"
2
2
import styled from "@emotion/styled"
3
3
import { graphql , PageProps } from "gatsby"
4
4
import { useIntl } from "react-intl"
@@ -22,11 +22,15 @@ import {
22
22
23
23
import { getLocaleTimestamp , INVALID_DATETIME } from "../../utils/time"
24
24
25
- import foreignTutorials from "../../data/externalTutorials.json"
25
+ import externalTutorials from "../../data/externalTutorials.json"
26
26
import FeedbackCard from "../../components/FeedbackCard"
27
27
import { getSkillTranslationId , Skill } from "../../components/TutorialMetadata"
28
28
import { Context } from "../../types"
29
29
import { Lang } from "../../utils/languages"
30
+ import {
31
+ filterTutorialsByLang ,
32
+ getSortedTutorialTagsForLang ,
33
+ } from "../../utils/tutorials"
30
34
31
35
const SubSlogan = styled . p `
32
36
font-size: 1.25rem;
@@ -217,7 +221,7 @@ const published = (locale: string, published: string) => {
217
221
) : null
218
222
}
219
223
220
- interface IExternalTutorial {
224
+ export interface IExternalTutorial {
221
225
url : string
222
226
title : string
223
227
description : string
@@ -230,7 +234,7 @@ interface IExternalTutorial {
230
234
publishDate : string
231
235
}
232
236
233
- interface ITutorial {
237
+ export interface ITutorial {
234
238
to : string
235
239
title : string
236
240
description : string
@@ -243,7 +247,7 @@ interface ITutorial {
243
247
isExternal : boolean
244
248
}
245
249
246
- interface ITutorialsState {
250
+ export interface ITutorialsState {
247
251
activeTagNames : Array < string >
248
252
filteredTutorials : Array < ITutorial >
249
253
}
@@ -252,131 +256,53 @@ const TutorialsPage = ({
252
256
data,
253
257
pageContext,
254
258
} : PageProps < Queries . DevelopersTutorialsPageQuery , Context > ) => {
255
- const intl = useIntl ( )
256
- // Filter tutorials by language and map to object
257
- const internalTutorials = data . allTutorials . nodes . map < ITutorial > (
258
- ( tutorial ) => ( {
259
- to :
260
- tutorial ?. fields ?. slug ?. substr ( 0 , 3 ) === "/en"
261
- ? tutorial . fields . slug . substr ( 3 )
262
- : tutorial . fields ?. slug || "/" ,
263
- title : tutorial ?. frontmatter ?. title || "" ,
264
- description : tutorial ?. frontmatter ?. description || "" ,
265
- author : tutorial ?. frontmatter ?. author || "" ,
266
- tags : tutorial ?. frontmatter ?. tags ?. map ( ( tag ) =>
267
- ( tag || "" ) . toLowerCase ( ) . trim ( )
259
+ const filteredTutorialsByLang = useMemo (
260
+ ( ) =>
261
+ filterTutorialsByLang (
262
+ data . allTutorials . nodes ,
263
+ externalTutorials ,
264
+ pageContext . locale
268
265
) ,
269
- skill : tutorial ?. frontmatter ?. skill as Skill ,
270
- timeToRead : tutorial ?. fields ?. readingTime ?. minutes
271
- ? Math . round ( tutorial ?. fields ?. readingTime ?. minutes )
272
- : null ,
273
- published : tutorial ?. frontmatter ?. published ,
274
- lang : tutorial ?. frontmatter ?. lang || "en" ,
275
- isExternal : false ,
276
- } )
266
+ [ pageContext . locale ]
277
267
)
278
268
279
- const externalTutorials = foreignTutorials . map < ITutorial > (
280
- ( tutorial : IExternalTutorial ) => ( {
281
- to : tutorial . url ,
282
- title : tutorial . title ,
283
- description : tutorial . description ,
284
- author : tutorial . author ,
285
- tags : tutorial . tags . map ( ( tag ) => tag . toLowerCase ( ) . trim ( ) ) ,
286
- skill : tutorial . skillLevel as Skill ,
287
- timeToRead : Number ( tutorial . timeToRead ) ,
288
- published : new Date ( tutorial . publishDate ) . toISOString ( ) ,
289
- lang : tutorial . lang || "en" ,
290
- isExternal : true ,
291
- } )
269
+ const allTags = useMemo (
270
+ ( ) => getSortedTutorialTagsForLang ( filteredTutorialsByLang ) ,
271
+ [ filteredTutorialsByLang ]
292
272
)
293
273
294
- const allTutorials : Array < ITutorial > = [
295
- ...externalTutorials ,
296
- ...internalTutorials ,
297
- ]
298
-
299
- const hasTutorialsCheck = allTutorials . some (
300
- ( tutorial ) => tutorial . lang === pageContext . language
274
+ const intl = useIntl ( )
275
+ const [ isModalOpen , setModalOpen ] = useState ( false )
276
+ const [ filteredTutorials , setFilteredTutorials ] = useState (
277
+ filteredTutorialsByLang
301
278
)
279
+ const [ selectedTags , setSelectedTags ] = useState < Array < string > > ( [ ] )
302
280
303
- const filteredTutorials = allTutorials
304
- . filter ( ( tutorial ) =>
305
- hasTutorialsCheck
306
- ? tutorial . lang === pageContext . language
307
- : tutorial . lang === "en"
308
- )
309
- . sort ( ( a , b ) => {
310
- if ( a . published && b . published ) {
311
- return new Date ( b . published ) . getTime ( ) - new Date ( a . published ) . getTime ( )
312
- }
313
- // Dont order if no published is present
314
- return 0
315
- } )
316
-
317
- // Tally all subject tag counts
318
- const tagsConcatenated : Array < string > = [ ]
319
- for ( const tutorial of filteredTutorials ) {
320
- if ( tutorial . tags ) {
321
- tagsConcatenated . push ( ...tutorial . tags )
281
+ useEffect ( ( ) => {
282
+ let tutorials = filteredTutorialsByLang
283
+
284
+ if ( selectedTags . length ) {
285
+ tutorials = tutorials . filter ( ( tutorial ) => {
286
+ return selectedTags . every ( ( tag ) => ( tutorial . tags || [ ] ) . includes ( tag ) )
287
+ } )
322
288
}
323
- }
324
289
325
- const allTags = tagsConcatenated . map ( ( tag ) => ( { name : tag , totalCount : 1 } ) )
326
- const sanitizedAllTags = Array . from (
327
- allTags . reduce (
328
- ( m , { name, totalCount } ) =>
329
- m . set (
330
- name . toLowerCase ( ) . trim ( ) ,
331
- ( m . get ( name . toLowerCase ( ) . trim ( ) ) || 0 ) + totalCount
332
- ) ,
333
- new Map ( )
334
- ) ,
335
- ( [ name , totalCount ] ) => ( { name, totalCount } )
336
- ) . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
337
-
338
- const [ state , setState ] = useState < ITutorialsState > ( {
339
- activeTagNames : [ ] ,
340
- filteredTutorials : filteredTutorials ,
341
- } )
342
-
343
- const clearActiveTags = ( ) => {
344
- setState ( {
345
- activeTagNames : [ ] ,
346
- filteredTutorials : filteredTutorials ,
347
- } )
348
- }
290
+ setFilteredTutorials ( tutorials )
291
+ } , [ selectedTags ] )
349
292
350
293
const handleTagSelect = ( tagName : string ) => {
351
- const activeTagNames = state . activeTagNames
294
+ const tempSelectedTags = selectedTags
352
295
353
- // Add or remove the selected tag
354
- const index = activeTagNames . indexOf ( tagName )
296
+ const index = tempSelectedTags . indexOf ( tagName )
355
297
if ( index > - 1 ) {
356
- activeTagNames . splice ( index , 1 )
298
+ tempSelectedTags . splice ( index , 1 )
357
299
} else {
358
- activeTagNames . push ( tagName )
300
+ tempSelectedTags . push ( tagName )
359
301
}
360
302
361
- // If no tags are active, show all tutorials, otherwise filter by active tag
362
- let filteredTutorials = allTutorials
363
- if ( activeTagNames . length > 0 ) {
364
- filteredTutorials = filteredTutorials . filter ( ( tutorial ) => {
365
- for ( const tag of activeTagNames ) {
366
- if ( ! tutorial . tags ?. includes ( tag ) ) {
367
- return false
368
- }
369
- }
370
- return true
371
- } )
372
- }
373
- setState ( { activeTagNames, filteredTutorials } )
303
+ setSelectedTags ( [ ...tempSelectedTags ] )
374
304
}
375
305
376
- const hasActiveTags = state . activeTagNames . length > 0
377
- const hasNoTutorials = state . filteredTutorials . length === 0
378
- const [ isModalOpen , setModalOpen ] = useState ( false )
379
-
380
306
return (
381
307
< StyledPage >
382
308
< PageMetadata
@@ -454,28 +380,28 @@ const TutorialsPage = ({
454
380
< TutorialContainer >
455
381
< TagsContainer >
456
382
< TagContainer >
457
- { sanitizedAllTags . map ( ( tag ) => {
458
- const name = `${ tag . name } (${ tag . totalCount } )`
459
- const isActive = state . activeTagNames . includes ( tag . name )
383
+ { Object . entries ( allTags ) . map ( ( [ tagName , tagCount ] ) => {
384
+ const name = `${ tagName } (${ tagCount } )`
385
+ const isActive = selectedTags . includes ( tagName )
460
386
return (
461
387
< Tag
462
388
name = { name }
463
389
key = { name }
464
390
isActive = { isActive }
465
391
shouldShowIcon = { false }
466
392
onClick = { handleTagSelect }
467
- value = { tag . name }
393
+ value = { tagName }
468
394
/>
469
395
)
470
396
} ) }
471
- { hasActiveTags && (
472
- < ClearLink onClick = { clearActiveTags } >
397
+ { selectedTags . length > 0 && (
398
+ < ClearLink onClick = { ( ) => setSelectedTags ( [ ] ) } >
473
399
< Translation id = "page-find-wallet-clear" />
474
400
</ ClearLink >
475
401
) }
476
402
</ TagContainer >
477
403
</ TagsContainer >
478
- { hasNoTutorials && (
404
+ { filteredTutorials . length === 0 && (
479
405
< ResultsContainer >
480
406
< Emoji text = ":crying_face:" size = { 3 } mb = { `2em` } mt = { `2em` } />
481
407
< h2 >
@@ -486,7 +412,7 @@ const TutorialsPage = ({
486
412
</ p >
487
413
</ ResultsContainer >
488
414
) }
489
- { state . filteredTutorials . map ( ( tutorial ) => {
415
+ { filteredTutorials . map ( ( tutorial ) => {
490
416
return (
491
417
< TutorialCard
492
418
key = { tutorial . to }
0 commit comments