forked from code-dot-org/code-dot-org
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtutorialExplorer.js
More file actions
865 lines (776 loc) · 27.5 KB
/
Copy pathtutorialExplorer.js
File metadata and controls
865 lines (776 loc) · 27.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
/**
* Entry point to build a bundle containing a set of globals used when displaying
* tutorialExplorer. Includes the TutorialExplorer React class.
*/
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import Immutable from 'immutable';
import FilterHeader from './filterHeader';
import FilterSet from './filterSet';
import TutorialSet from './tutorialSet';
import ToggleAllTutorialsButton from './toggleAllTutorialsButton';
import Search from './search';
import {
isTutorialSortByFieldNamePopularity,
TutorialsSortByOptions,
TutorialsSortByFieldNames,
TutorialsOrgName,
mobileCheck,
DoNotShow,
orgNameCodeOrg,
orgNameMinecraft,
} from './util';
import {
getResponsiveContainerWidth,
isResponsiveCategoryInactive,
getResponsiveValue,
} from './responsive';
import i18n from '@cdo/tutorialExplorer/locale';
import _ from 'lodash';
import queryString from 'query-string';
import {StickyContainer} from 'react-sticky';
export default class TutorialExplorer extends React.Component {
static propTypes = {
tutorials: PropTypes.array.isRequired,
filterGroups: PropTypes.array.isRequired,
initialFilters: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string))
.isRequired,
hideFilters: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)),
locale: PropTypes.string.isRequired,
showSortDropdown: PropTypes.bool.isRequired,
disabledTutorials: PropTypes.arrayOf(PropTypes.string).isRequired,
defaultSortBy: PropTypes.oneOf(Object.keys(TutorialsSortByOptions))
.isRequired,
};
constructor(props) {
super(props);
this.shouldScrollToTop = false;
let filters = {};
for (const filterGroupName in props.filterGroups) {
const filterGroup = props.filterGroups[filterGroupName];
filters[filterGroup.name] = [];
const initialFiltersForGroup = props.initialFilters[filterGroup.name];
if (initialFiltersForGroup) {
filters[filterGroup.name] = initialFiltersForGroup;
}
}
const sortBy = props.defaultSortBy;
const orgName = TutorialsOrgName.all;
const defaultSearchTerm = '';
const filteredTutorials = this.filterTutorialSet(
filters,
sortBy,
orgName,
defaultSearchTerm
);
const filteredTutorialsForLocale = this.filterTutorialSetForLocale();
const showingAllTutorials = this.isLocaleEnglish();
this.state = {
filters: filters,
filteredTutorials: filteredTutorials,
filteredTutorialsCount: filteredTutorials.length,
filteredTutorialsForLocale: filteredTutorialsForLocale,
windowWidth: $(window).width(),
windowHeight: $(window).height(),
mobileLayout: isResponsiveCategoryInactive('md'),
showingModalFilters: false,
sortBy: sortBy,
orgName: orgName,
showingAllTutorials: showingAllTutorials,
searchTerm: defaultSearchTerm,
};
}
handleSearchTerm = searchTerm => {
const filteredTutorials = this.filterTutorialSet(
this.state.filters,
this.state.sortBy,
this.state.orgName,
searchTerm
);
this.setState({
searchTerm,
filteredTutorials,
filteredTutorialsCount: filteredTutorials.length,
});
};
/**
* Called when a filter in a filter group has its checkbox
* checked or unchecked.
*
* @param {string} filterGroup - The name of the filter group.
* @param {string} filterEntry - The name of the filter entry.
* @param {bool} value - Whether the entry was checked or not.
*/
handleUserInputFilter = (filterGroup, filterEntry, value) => {
const state = Immutable.fromJS(this.state);
let newState = {};
if (
this.props.filterGroups.find(item => item.name === filterGroup)
.singleEntry
) {
newState = state.updateIn(['filters', filterGroup], arr => [filterEntry]);
} else if (value) {
// Add value to end of array.
newState = state.updateIn(['filters', filterGroup], arr =>
arr.push(filterEntry)
);
} else {
// Find and remove specific value from array.
const itemIndex = this.state.filters[filterGroup].indexOf(filterEntry);
newState = state.updateIn(['filters', filterGroup], arr =>
arr.splice(itemIndex, 1)
);
}
newState = newState.toJS();
const filteredTutorials = this.filterTutorialSet(
newState.filters,
this.state.sortBy,
this.state.orgName,
this.state.searchTerm
);
this.setState({
...newState,
filteredTutorials,
filteredTutorialsCount: filteredTutorials.length,
});
};
/**
* Called when the sort order is changed via dropdown.
*
* @param {SortBy} value - The new sort order.
*/
handleUserInputSortBy = value => {
const filteredTutorials = this.filterTutorialSet(
this.state.filters,
value,
this.state.orgName,
this.state.searchTerm
);
this.setState({
filteredTutorials,
filteredTutorialsCount: filteredTutorials.length,
sortBy: value,
});
this.scrollToTop();
};
/**
* Called when the org name is changed via dropdown.
*/
handleUserInputOrgName = value => {
const filteredTutorials = this.filterTutorialSet(
this.state.filters,
this.state.sortBy,
value,
this.state.searchTerm
);
this.setState({
filteredTutorials,
filteredTutorialsCount: filteredTutorials.length,
orgName: value,
});
this.scrollToTop();
};
/*
* Now that we've re-rendered changes, check to see if there's a pending
* scroll to the top of all tutorials.
* jQuery is used to do the scrolling, which is a little unusual, but
* ensures a smooth, well-eased movement.
*/
componentDidUpdate() {
if (this.shouldScrollToTop) {
$('html, body').animate({scrollTop: $(this.allTutorials).offset().top});
this.shouldScrollToTop = false;
}
}
/**
* Set up a smooth scroll to the top of all tutorials once we've re-rendered the
* relevant changes.
* Note that if that next render never comes, we won't actually do the scroll.
*/
scrollToTop() {
this.shouldScrollToTop = true;
}
/**
* Given a sort by choice (popularityrank or displayweight) and a grade range,
* return the field name from the tutorials data that should used for sorting.
*/
getSortByFieldName(sortBy, grade) {
let sortByFieldName;
const gradeToDisplayWeightSortByFieldName = {
all: TutorialsSortByFieldNames.displayweight,
pre: TutorialsSortByFieldNames.displayweight_pre,
'2-5': TutorialsSortByFieldNames.displayweight_25,
'6-8': TutorialsSortByFieldNames.displayweight_middle,
'9+': TutorialsSortByFieldNames.displayweight_high,
};
const gradeToPopularityRankSortByFieldName = {
all: TutorialsSortByFieldNames.popularityrank,
pre: TutorialsSortByFieldNames.popularityrank_pre,
'2-5': TutorialsSortByFieldNames.popularityrank_25,
'6-8': TutorialsSortByFieldNames.popularityrank_middle,
'9+': TutorialsSortByFieldNames.popularityrank_high,
};
// If we're sorting by recommendation (a.k.a. displayweight) then find the
// right set of data to match the currently-selected grade.
if (sortBy === TutorialsSortByOptions.displayweight) {
sortByFieldName = gradeToDisplayWeightSortByFieldName[grade];
} else {
sortByFieldName = gradeToPopularityRankSortByFieldName[grade];
}
return sortByFieldName;
}
/*
* The main tutorial set is returned with the given filters and sort order.
*
* Whether en or non-en user, this filters as though the user is of "en-US" locale.
*/
filterTutorialSet(filters, sortBy, orgName, searchTerm) {
const grade = filters.grade[0];
const filterProps = {
filters: filters,
hideFilters: this.props.hideFilters,
locale: 'en-US',
orgName: orgName,
sortByFieldName: this.getSortByFieldName(sortBy, grade),
searchTerm: searchTerm,
};
return TutorialExplorer.filterTutorials(this.props.tutorials, filterProps);
}
/*
* The extra set of tutorials for a specific locale, shown at top for non-en user
* with no filter options.
*/
filterTutorialSetForLocale() {
const filterProps = {
sortByFieldName: this.props.defaultSortBy,
};
filterProps.specificLocale = true;
filterProps.locale = this.props.locale;
return TutorialExplorer.filterTutorials(this.props.tutorials, filterProps);
}
getUniqueOrgNames() {
return TutorialExplorer.getUniqueOrgNamesFromTutorials(
this.props.tutorials
);
}
componentDidMount() {
window.addEventListener('resize', _.debounce(this.onResize, 100));
}
showModalFilters = () => {
this.setState({showingModalFilters: true});
if (this.state.mobileLayout) {
this.scrollToTop();
}
};
hideModalFilters = () => {
this.setState({showingModalFilters: false});
if (this.state.mobileLayout) {
this.scrollToTop();
}
};
showAllTutorials = () => {
this.setState({showingAllTutorials: true});
};
hideAllTutorials = () => {
this.setState({showingAllTutorials: false});
};
shouldShowFilters() {
return !this.state.mobileLayout || this.state.showingModalFilters;
}
shouldShowTutorials() {
return !this.state.mobileLayout || !this.state.showingModalFilters;
}
shouldShowTutorialsForLocale() {
return !this.isLocaleEnglish();
}
shouldShowAllTutorialsToggleButton() {
return !this.isLocaleEnglish();
}
isLocaleEnglish() {
return this.props.locale.substring(0, 2) === 'en';
}
/**
* Called when the window resizes. Look to see if width/height changed, then
* call adjustTopPaneHeight as our maxHeight may need adjusting.
*/
onResize = () => {
const windowWidth = $(window).width();
const windowHeight = $(window).height();
// We fire window resize events when the grippy is dragged so that non-React
// controlled components are able to rerender the editor. If width/height
// didn't change, we don't need to do anything else here
if (
windowWidth === this.state.windowWidth &&
windowHeight === this.state.windowHeight
) {
return;
}
this.setState({
windowWidth: $(window).width(),
windowHeight: $(window).height(),
});
this.setState({mobileLayout: isResponsiveCategoryInactive('md')});
};
/**
* Filters a given array of tutorials by the given filter props.
*
* It goes through all active filter categories. If no filters are set for
* a filter group, then that item will default to showing, so long as no other
* filter group prevents it from showing.
* hideFilters is an explicit list of filters that we actually hide if matched.
* But if we do have a filter set for a filter group, and the tutorial is tagged
* for that filter group, then at least one of the active filters must match a tag.
* e.g. If the user chooses two platforms, then at least one of the platforms
* must match a platform tag on the tutorial.
* A similar check for language is done first.
* In the case that filterProps.specificLocale is true, we do something slightly
* different. We don't show tutorials that don't have any language tags, and we
* reject tutorials that don't have the current locale explicitly listed. This
* allows us to return a set of tutorials that have explicit support for the
* current locale.
*
* @param {Array} tutorials - Array of tutorials. Each contains a variety of
* strings, each of which is a list of tags separated by commas, no spaces.
* @param {object} filterProps - Object containing filter properties.
* @param {string} filterProps.locale - The current locale.
* @param {bool} filterProps.specificLocale - Whether we filter to only allow
* through tutorials matching the current locale.
* @param {object} filterProps.filters - Contains arrays of strings identifying
* the currently active filters. Each array is named for its filter group.
*/
static filterTutorials(tutorials, filterProps) {
const {
locale,
specificLocale,
orgName,
filters,
hideFilters,
sortByFieldName,
searchTerm,
} = filterProps;
const cleanSearchTerm = searchTerm?.toLowerCase()?.trim();
const filteredTutorials = tutorials
.filter(tutorial => {
// Check that the tutorial isn't marked as DoNotShow. If it does,
// it's hidden.
if (tutorial.tags.split(',').indexOf(DoNotShow) !== -1) {
return false;
}
// First check that the tutorial language doesn't exclude it immediately.
// If the tags contain some languages, and we don't have a match, then
// hide the tutorial.
if (locale && tutorial.languages_supported) {
const languageTags = tutorial.languages_supported.split(',');
if (
languageTags.length > 0 &&
languageTags.indexOf(locale) === -1 &&
languageTags.indexOf(locale.substring(0, 2)) === -1
) {
return false;
}
} else if (specificLocale) {
// If the tutorial doesn't have language tags, but we're only looking
// for specific matches to our current locale, then don't show this
// tutorial. i.e. don't let non-locale-specific tutorials through.
return false;
}
// If we are showing an explicit orgname, then filter if it doesn't
// match. Make an exception for Minecraft so that it shows when
// Code.org is selected.
if (
orgName &&
orgName !== TutorialsOrgName.all &&
tutorial.orgname !== orgName &&
!(orgName === orgNameCodeOrg && tutorial.orgname === orgNameMinecraft)
) {
return false;
}
if (
searchTerm &&
!(
tutorial.name?.toLowerCase().includes(cleanSearchTerm) ||
tutorial.longdescription?.toLowerCase().includes(cleanSearchTerm)
)
) {
return false;
}
// If we are explicitly hiding a matching filter, then don't show the
// tutorial.
for (const filterGroupName in hideFilters) {
const tutorialTags = tutorial['tags_' + filterGroupName];
const filterGroup = hideFilters[filterGroupName];
if (
filterGroup.length !== 0 &&
tutorialTags &&
tutorialTags.length > 0 &&
TutorialExplorer.findMatchingTag(filterGroup, tutorialTags)
) {
return false;
}
}
// If we miss any active filter group, then we don't show the tutorial.
let filterGroupsSatisfied = true;
for (const filterGroupName in filters) {
const tutorialTags = tutorial['tags_' + filterGroupName];
const filterGroup = filters[filterGroupName];
if (
filterGroup.length !== 0 &&
tutorialTags &&
tutorialTags.length > 0 &&
!TutorialExplorer.findMatchingTag(filterGroup, tutorialTags)
) {
filterGroupsSatisfied = false;
}
}
return filterGroupsSatisfied;
})
.sort((tutorial1, tutorial2) => {
if (isTutorialSortByFieldNamePopularity(sortByFieldName)) {
return tutorial1[sortByFieldName] - tutorial2[sortByFieldName];
} else {
return tutorial2[sortByFieldName] - tutorial1[sortByFieldName];
}
});
return filteredTutorials;
}
/* Given a filter group, and the tutorial's relevant tags for that filter group,
* see if there's at least a single match.
* @param {Array} filterGroup - Array of strings, each of which is a selected filter
* for the group. e.g. ["beginner", "experienced"].
* @param {string} tutorialTags - Comma-separated tags for a tutorial.
* e.g. "beginner,experienced".
* @return {bool} - true if the tutorial had at least one tag matching at least
* one of the filterGroup's values.
*/
static findMatchingTag(filterGroup, tutorialTags) {
return filterGroup.some(
filterName => tutorialTags.split(',').indexOf(filterName) !== -1
);
}
/* Returns an array of unique organization names from the set of tutorials,
* sorted alphabetically.
*
* @param {Array} tutorials - Array of tutorials.
* @return {Array} - Array of strings.
*/
static getUniqueOrgNamesFromTutorials(tutorials) {
// Filter out tutorials with DoNotShow as either tag or organization name.
let availableTutorials = tutorials.filter(t => {
return (
t.tags.split(',').indexOf(DoNotShow) === -1 && t.orgname !== DoNotShow
);
});
// Construct array of unique org names from the tutorials.
let uniqueOrgNames = _.uniq(availableTutorials.map(t => t.orgname));
// Sort the unique org names alphabetically, case-insensitive.
uniqueOrgNames.sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
return uniqueOrgNames;
}
render() {
const bottomLinksContainerStyle = {
...styles.bottomLinksContainer,
textAlign: getResponsiveValue({xs: 'left', md: 'right'}),
visibility: this.shouldShowTutorials() ? 'visible' : 'hidden',
};
const grade = this.state.filters.grade[0];
return (
<div
style={{
width: getResponsiveContainerWidth(),
margin: '0 auto',
paddingBottom: 0,
}}
>
{this.shouldShowTutorialsForLocale() && (
<div>
<h1>{i18n.headingTutorialsYourLanguage()}</h1>
{this.state.filteredTutorialsForLocale.length === 0 &&
i18n.noTutorialsYourLanguage()}
{this.state.filteredTutorialsForLocale.length > 0 && (
<TutorialSet
tutorials={this.state.filteredTutorialsForLocale}
specificLocale={true}
localeEnglish={false}
disabledTutorials={this.props.disabledTutorials}
grade={grade}
/>
)}
</div>
)}
{this.shouldShowAllTutorialsToggleButton() && (
<ToggleAllTutorialsButton
showAllTutorials={this.showAllTutorials}
hideAllTutorials={this.hideAllTutorials}
showingAllTutorials={this.state.showingAllTutorials}
/>
)}
{this.state.showingAllTutorials && (
<StickyContainer>
<div ref={allTutorials => (this.allTutorials = allTutorials)}>
<FilterHeader
mobileLayout={this.state.mobileLayout}
filterGroups={this.props.filterGroups}
selection={this.state.filters}
onUserInputFilter={this.handleUserInputFilter}
filteredTutorialsCount={this.state.filteredTutorialsCount}
showingModalFilters={this.state.showingModalFilters}
showModalFilters={this.showModalFilters}
hideModalFilters={this.hideModalFilters}
/>
<div style={{clear: 'both'}} />
{this.shouldShowFilters() && (
<div
style={{
float: 'left',
width: getResponsiveValue({xs: 100, md: 20}),
}}
>
<Search
onChange={this.handleSearchTerm}
showClearIcon={this.state.searchTerm !== ''}
/>
<FilterSet
mobileLayout={this.state.mobileLayout}
uniqueOrgNames={this.getUniqueOrgNames()}
orgName={this.state.orgName}
showSortDropdown={this.props.showSortDropdown}
defaultSortBy={this.props.defaultSortBy}
sortBy={this.state.sortBy}
filterGroups={this.props.filterGroups}
selection={this.state.filters}
onUserInputFilter={this.handleUserInputFilter}
onUserInputOrgName={this.handleUserInputOrgName}
onUserInputSortBy={this.handleUserInputSortBy}
/>
</div>
)}
<div
style={{
float: 'left',
width: getResponsiveValue({xs: 100, md: 80}),
}}
>
{this.shouldShowTutorials() && (
<TutorialSet
tutorials={this.state.filteredTutorials}
localeEnglish={this.isLocaleEnglish()}
disabledTutorials={this.props.disabledTutorials}
grade={grade}
/>
)}
</div>
<div style={bottomLinksContainerStyle}>
<div style={styles.bottomLinksLinkFirst}>
<a
style={styles.bottomLinksLink}
href="https://hourofcode.com/activity-guidelines"
>
{i18n.bottomGuidelinesLink()}
</a>
</div>
<div>
<a
style={styles.bottomLinksLink}
href="https://support.code.org/hc/en-us/articles/115001306531-How-can-students-with-special-needs-or-disabilities-participate-"
>
{i18n.bottomSpecialNeedsLink()}
</a>
</div>
</div>
</div>
</StickyContainer>
)}
</div>
);
}
}
const styles = {
bottomLinksContainer: {
padding: '10px 7px 40px 7px',
fontSize: 13,
lineHeight: '17px',
clear: 'both',
},
bottomLinksLink: {
fontFamily: '"Gotham 5r", sans-serif',
},
bottomLinksLinkFirst: {
paddingBottom: 10,
},
};
function getFilters({mobile}) {
const filters = [
{
name: 'grade',
text: i18n.filterGrades(),
headerOnDesktop: true,
singleEntry: true,
entries: [
{name: 'all', text: i18n.filterGradesAll()},
{name: 'pre', text: i18n.filterGradesPre()},
{name: '2-5', text: i18n.filterGrades25()},
{name: '6-8', text: i18n.filterGrades68()},
{name: '9+', text: i18n.filterGrades9()},
],
},
{
name: 'student_experience',
text: i18n.filterStudentExperience(),
headerOnDesktop: true,
singleEntry: true,
entries: [
{name: 'beginner', text: i18n.filterStudentExperienceBeginner()},
{name: 'comfortable', text: i18n.filterStudentExperienceComfortable()},
],
},
{
name: 'platform',
text: i18n.filterPlatform(),
entries: [
{name: 'computers', text: i18n.filterPlatformComputers()},
{name: 'android', text: i18n.filterPlatformAndroid()},
{name: 'ios', text: i18n.filterPlatformIos()},
{
name: 'robotics',
text: i18n.filterPlatformRobotics(),
},
{name: 'no-internet', text: i18n.filterPlatformNoInternet()},
{name: 'no-computers', text: i18n.filterPlatformNoComputers()},
],
},
{
name: 'subject',
text: i18n.filterTopics(),
entries: [
{name: 'science', text: i18n.filterTopicsScience()},
{name: 'math', text: i18n.filterTopicsMath()},
{name: 'history', text: i18n.filterTopicsHistory()},
{name: 'la', text: i18n.filterTopicsLa()},
{name: 'art', text: i18n.filterTopicsArt()},
{name: 'cs-only', text: i18n.filterTopicsCsOnly()},
],
},
{
name: 'activity_type',
text: i18n.filterActivityType(),
entries: [
{
name: 'online-tutorial',
text: i18n.filterActivityTypeOnlineTutorial(),
},
{name: 'lesson-plan', text: i18n.filterActivityTypeLessonPlan()},
],
},
{
name: 'length',
text: i18n.filterLength(),
entries: [
{name: '1hour', text: i18n.filterLength1Hour()},
{name: '1hour-follow', text: i18n.filterLength1HourFollow()},
{name: 'few-hours', text: i18n.filterLengthFewHours()},
],
},
{
name: 'accessibility',
text: i18n.filterAccessibility(),
entries: [
{name: 'screenreader', text: i18n.filterAccessibilityScreenReader()},
{name: 'tts', text: i18n.filterAccessibilityTTS()},
{name: 'keyboard', text: i18n.filterAccessibilityKeyboard()},
{name: 'captions', text: i18n.filterAccessibilityCaptions()},
{name: 'highcontrast', text: i18n.filterAccessibilityHighContrast()},
],
},
{
name: 'programming_language',
text: i18n.filterProgrammingLanguage(),
entries: [
{name: 'blocks', text: i18n.filterProgrammingLanguageBlocks()},
{name: 'typing', text: i18n.filterProgrammingLanguageTyping()},
{name: 'other', text: i18n.filterProgrammingLanguageOther()},
],
},
];
const initialFilters = {
student_experience: ['beginner'],
grade: ['all'],
};
const hideFilters = {};
if (mobile) {
initialFilters.platform = ['android', 'ios'];
}
return {filters, initialFilters, hideFilters};
}
/*
* Parse URL parameters to retrieve an override of initialFilters.
*
* @param {Array} filters - Array of filterGroup objects.
*
* @return {object} - Returns an object containing arrays of strings. Each
* array is named for a filterGroup name, and each string inside is named
* for a filter entry. Note that this is not currently allow-listed against
* our known name of filterGroups/entries, but invalid entries should be
* ignored in the filtering user experience.
*/
function getUrlParameters(filters) {
// Create a result object that has a __proto__ so that React validation will work
// properly.
let parametersObject = {};
let parameters = queryString.parse(location.search);
for (const name in parameters) {
const filterGroup = filters.find(item => item.name === name);
// Validate filterGroup name.
if (filterGroup) {
let entryNames = [];
if (typeof parameters[name] === 'string') {
// Convert item with single filter entry into array containing the string.
entryNames = [parameters[name]];
} else {
entryNames = parameters[name];
}
for (const entry in entryNames) {
const entryName = entryNames[entry];
// Validate entry name.
if (filterGroup.entries.find(item => item.name === entryName)) {
if (!parametersObject[name]) {
parametersObject[name] = [];
}
parametersObject[name].push(entryName);
}
}
}
}
return parametersObject;
}
window.TutorialExplorerManager = function (options) {
options.mobile = mobileCheck();
let {filters, initialFilters, hideFilters} = getFilters(options);
// Check for URL-based override of initialFilters.
const providedParameters = getUrlParameters(filters);
if (!_.isEmpty(providedParameters)) {
initialFilters = providedParameters;
}
// The caller can provide defaultSortByPopularity, and when true, the default sort will
// be by popularity. Otherwise, the default sort will be by display weight.
const defaultSortBy = options.defaultSortByPopularity
? TutorialsSortByOptions.popularityrank
: TutorialsSortByOptions.displayweight;
this.renderToElement = function (element) {
ReactDOM.render(
<TutorialExplorer
tutorials={options.tutorials}
filterGroups={filters}
initialFilters={initialFilters}
hideFilters={hideFilters}
locale={options.locale}
showSortDropdown={options.showSortDropdown}
disabledTutorials={options.disabledTutorials}
defaultSortBy={defaultSortBy}
/>,
element
);
};
};