Skip to content

Commit 82f6166

Browse files
committed
feat: migrate resource learning index to jaspr
1 parent 5b179ee commit 82f6166

File tree

8 files changed

+544
-611
lines changed

8 files changed

+544
-611
lines changed

site/lib/jaspr_options.dart

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,25 @@ import 'package:docs_flutter_dev_site/src/components/client/dartpad_injector.dar
1313
as prefix2;
1414
import 'package:docs_flutter_dev_site/src/components/client/download_latest_button.dart'
1515
as prefix3;
16-
import 'package:docs_flutter_dev_site/src/components/client/on_this_page_button.dart'
16+
import 'package:docs_flutter_dev_site/src/components/client/learning_resource_filters.dart'
1717
as prefix4;
18-
import 'package:docs_flutter_dev_site/src/components/header/menu_toggle.dart'
18+
import 'package:docs_flutter_dev_site/src/components/client/learning_resource_filters_sidebar.dart'
1919
as prefix5;
20-
import 'package:docs_flutter_dev_site/src/components/header/site_switcher.dart'
20+
import 'package:docs_flutter_dev_site/src/components/client/on_this_page_button.dart'
2121
as prefix6;
22-
import 'package:docs_flutter_dev_site/src/components/header/theme_switcher.dart'
22+
import 'package:docs_flutter_dev_site/src/components/header/menu_toggle.dart'
2323
as prefix7;
24-
import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart'
24+
import 'package:docs_flutter_dev_site/src/components/header/site_switcher.dart'
2525
as prefix8;
26-
import 'package:docs_flutter_dev_site/src/components/copy_button.dart'
26+
import 'package:docs_flutter_dev_site/src/components/header/theme_switcher.dart'
2727
as prefix9;
28-
import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix10;
29-
import 'package:docs_flutter_dev_site/src/components/os_selector.dart'
28+
import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart'
29+
as prefix10;
30+
import 'package:docs_flutter_dev_site/src/components/copy_button.dart'
3031
as prefix11;
32+
import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix12;
33+
import 'package:docs_flutter_dev_site/src/components/os_selector.dart'
34+
as prefix13;
3135

3236
/// Default [JasprOptions] for use with your jaspr project.
3337
///
@@ -66,37 +70,47 @@ JasprOptions get defaultJasprOptions => JasprOptions(
6670
params: _prefix3DownloadLatestButton,
6771
),
6872

69-
prefix4.OnThisPageButton: ClientTarget<prefix4.OnThisPageButton>(
73+
prefix4.LearningResourceFilters:
74+
ClientTarget<prefix4.LearningResourceFilters>(
75+
'src/components/client/learning_resource_filters',
76+
),
77+
78+
prefix5.LearningResourceFiltersSidebar:
79+
ClientTarget<prefix5.LearningResourceFiltersSidebar>(
80+
'src/components/client/learning_resource_filters_sidebar',
81+
),
82+
83+
prefix6.OnThisPageButton: ClientTarget<prefix6.OnThisPageButton>(
7084
'src/components/client/on_this_page_button',
7185
),
7286

73-
prefix8.CookieNotice: ClientTarget<prefix8.CookieNotice>(
87+
prefix10.CookieNotice: ClientTarget<prefix10.CookieNotice>(
7488
'src/components/cookie_notice',
7589
),
7690

77-
prefix9.CopyButton: ClientTarget<prefix9.CopyButton>(
91+
prefix11.CopyButton: ClientTarget<prefix11.CopyButton>(
7892
'src/components/copy_button',
79-
params: _prefix9CopyButton,
93+
params: _prefix11CopyButton,
8094
),
8195

82-
prefix10.FeedbackComponent: ClientTarget<prefix10.FeedbackComponent>(
96+
prefix12.FeedbackComponent: ClientTarget<prefix12.FeedbackComponent>(
8397
'src/components/feedback',
84-
params: _prefix10FeedbackComponent,
98+
params: _prefix12FeedbackComponent,
8599
),
86100

87-
prefix5.MenuToggle: ClientTarget<prefix5.MenuToggle>(
101+
prefix7.MenuToggle: ClientTarget<prefix7.MenuToggle>(
88102
'src/components/header/menu_toggle',
89103
),
90104

91-
prefix6.SiteSwitcher: ClientTarget<prefix6.SiteSwitcher>(
105+
prefix8.SiteSwitcher: ClientTarget<prefix8.SiteSwitcher>(
92106
'src/components/header/site_switcher',
93107
),
94108

95-
prefix7.ThemeSwitcher: ClientTarget<prefix7.ThemeSwitcher>(
109+
prefix9.ThemeSwitcher: ClientTarget<prefix9.ThemeSwitcher>(
96110
'src/components/header/theme_switcher',
97111
),
98112

99-
prefix11.OsSelector: ClientTarget<prefix11.OsSelector>(
113+
prefix13.OsSelector: ClientTarget<prefix13.OsSelector>(
100114
'src/components/os_selector',
101115
),
102116
},
@@ -116,11 +130,11 @@ Map<String, dynamic> _prefix2DartPadInjector(prefix2.DartPadInjector c) => {
116130
Map<String, dynamic> _prefix3DownloadLatestButton(
117131
prefix3.DownloadLatestButton c,
118132
) => {'os': c.os, 'arch': c.arch};
119-
Map<String, dynamic> _prefix9CopyButton(prefix9.CopyButton c) => {
133+
Map<String, dynamic> _prefix11CopyButton(prefix11.CopyButton c) => {
120134
'toCopy': c.toCopy,
121135
'buttonText': c.buttonText,
122136
'classes': c.classes,
123137
'title': c.title,
124138
};
125-
Map<String, dynamic> _prefix10FeedbackComponent(prefix10.FeedbackComponent c) =>
139+
Map<String, dynamic> _prefix12FeedbackComponent(prefix12.FeedbackComponent c) =>
126140
{'issueUrl': c.issueUrl};

site/lib/src/analytics/analytics_web.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:meta/meta.dart';
66
import 'package:universal_web/js_interop.dart';
77
import 'package:universal_web/web.dart' as web;
88

9+
import '../util.dart';
910
import 'analytics.dart';
1011

1112
/// Web implementation of [Analytics].
@@ -15,6 +16,9 @@ import 'analytics.dart';
1516
final class AnalyticsImplementation extends Analytics {
1617
@override
1718
void sendEvent(String eventName, Map<String, Object?> parameters) {
19+
if (!productionBuild) {
20+
return;
21+
}
1822
final dataLayer = web.window['dataLayer'];
1923
if (dataLayer.isA<JSArray>()) {
2024
(dataLayer as JSArray).toDart.add(

site/lib/src/components/client/archive_table.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ class _ArchiveTableState extends State<ArchiveTable> {
144144
final archiveExtension = os == 'linux' ? 'tar.xz' : 'zip';
145145
return a(
146146
href:
147-
'${FlutterRelease.baseReleasesUrl}$channel/$os/'
148-
'flutter_${os}_${release.version}-$channel.$archiveExtension.intoto.jsonl',
147+
'${FlutterRelease.baseReleasesUrl}$channel/$os/flutter_${os}_'
148+
'${release.version}-$channel.$archiveExtension.intoto.jsonl',
149149
target: Target.blank,
150150
[text('Attestation bundle')],
151151
);
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:math';
6+
7+
import 'package:jaspr/jaspr.dart';
8+
import 'package:universal_web/js_interop.dart';
9+
import 'package:universal_web/web.dart' as web;
10+
11+
import '../../analytics/analytics.dart';
12+
import '../../models/learning_resource_model.dart';
13+
import '../util/global_event_listener.dart';
14+
import 'learning_resource_filters_sidebar.dart';
15+
16+
class ResourceInfo {
17+
ResourceInfo({
18+
required this.name,
19+
required this.type,
20+
required this.tags,
21+
required this.description,
22+
});
23+
24+
factory ResourceInfo.fromElement(web.Element element) {
25+
final dataType = element.getAttribute('data-type') ?? '';
26+
final dataTags = element.getAttribute('data-tags') ?? '';
27+
final dataDescription = element.getAttribute('data-description') ?? '';
28+
29+
return ResourceInfo(
30+
name: element.id,
31+
type: LearningResourceType.values.firstWhere(
32+
(type) => type.id == dataType,
33+
orElse: () => LearningResourceType.sample,
34+
),
35+
tags: dataTags
36+
.split(',')
37+
.map((t) => t.trim().toLowerCase())
38+
.map((tagId) {
39+
return LearningResourceTag.values
40+
.where((tag) => tag.id == tagId)
41+
.firstOrNull;
42+
})
43+
.nonNulls
44+
.toList(),
45+
description: dataDescription,
46+
);
47+
}
48+
49+
final String name;
50+
final LearningResourceType type;
51+
final List<LearningResourceTag> tags;
52+
final String description;
53+
}
54+
55+
@client
56+
class LearningResourceFilters extends StatefulComponent {
57+
const LearningResourceFilters({super.key});
58+
59+
@override
60+
State<LearningResourceFilters> createState() =>
61+
_LearningResourceFiltersState();
62+
}
63+
64+
class _LearningResourceFiltersState extends State<LearningResourceFilters> {
65+
String searchQuery = '';
66+
67+
FiltersNotifier get filters => LearningResourceFiltersSidebar.filters;
68+
69+
final List<ResourceInfo> resourceInfos = [];
70+
int filteredResourcesCount = 0;
71+
72+
@override
73+
void initState() {
74+
super.initState();
75+
76+
if (kIsWeb) {
77+
filters.addListener(setFilters);
78+
79+
final resourceGrid = web.document.getElementById('all-resources-grid');
80+
if (resourceGrid == null) {
81+
return;
82+
}
83+
84+
final resourceCards = resourceGrid.querySelectorAll('.card');
85+
loadResourceInfos(resourceCards);
86+
shuffleCards(resourceGrid);
87+
}
88+
}
89+
90+
void loadResourceInfos(web.NodeList resourceCards) {
91+
for (var i = 0; i < resourceCards.length; i++) {
92+
final element = resourceCards.item(i) as web.Element;
93+
final info = ResourceInfo.fromElement(element);
94+
resourceInfos.add(info);
95+
96+
element.addEventListener(
97+
'click',
98+
((web.Event event) {
99+
analytics.sendEvent('learning_resource_index_click', {
100+
'learning_resource_type': info.type.id,
101+
'learning_resource_title': info.name,
102+
});
103+
}).toJS,
104+
);
105+
}
106+
}
107+
108+
void shuffleCards(web.Element container) {
109+
final r = Random();
110+
final elements = container.childNodes;
111+
for (var i = elements.length; i > 0; i--) {
112+
final card = elements.item(r.nextInt(i));
113+
container.appendChild(card!);
114+
}
115+
}
116+
117+
/// Update the filter state and re-evaluate which resources to show.
118+
///
119+
/// Use like the `setState` method by passing a callback that updates
120+
/// the relevant state variables.
121+
///
122+
/// Example:
123+
///
124+
/// ```dart
125+
/// setFilters(() {
126+
/// searchQuery = '...';
127+
/// });
128+
/// ```
129+
void setFilters([void Function()? callback]) {
130+
setState(callback ?? () {});
131+
132+
final resourcesToShow = filters.filterResources(resourceInfos, searchQuery);
133+
filteredResourcesCount = resourcesToShow.length;
134+
for (final info in resourceInfos) {
135+
final element =
136+
web.document.getElementById(info.name) as web.HTMLElement?;
137+
if (element == null) {
138+
continue;
139+
}
140+
141+
if (resourcesToShow.contains(info)) {
142+
element.classList.remove('hidden');
143+
} else {
144+
element.classList.add('hidden');
145+
}
146+
}
147+
}
148+
149+
@override
150+
void dispose() {
151+
if (kIsWeb) {
152+
filters.removeListener(setFilters);
153+
}
154+
super.dispose();
155+
}
156+
157+
@override
158+
Component build(BuildContext context) {
159+
return div(id: 'resource-search-group', classes: 'chip-filters-group', [
160+
div(classes: 'top-row', [
161+
div(classes: 'search-wrapper', id: 'resource-search', [
162+
span(
163+
classes: 'material-symbols leading-icon',
164+
attributes: {'aria-hidden': 'true', 'translate': 'no'},
165+
[text('search')],
166+
),
167+
input(
168+
type: InputType.search,
169+
attributes: {
170+
'placeholder': 'Try "button" or "networking"...',
171+
'aria-label': 'Search learning resources by name and category',
172+
},
173+
value: searchQuery,
174+
onInput: (value) {
175+
setFilters(() {
176+
searchQuery = value as String;
177+
});
178+
},
179+
),
180+
]),
181+
GlobalEventListener(
182+
onClick: (event) {
183+
final target = event.target as web.Element?;
184+
// If clicking outside the filters or toggle, close the filters.
185+
if (target?.closest('#resource-filter-group-wrapper') == null &&
186+
target?.closest('.show-filters-button') == null) {
187+
final toggle =
188+
web.document.getElementById('open-filter-toggle')
189+
as web.HTMLInputElement?;
190+
toggle?.checked = false;
191+
}
192+
},
193+
button(
194+
classes: 'icon-button show-filters-button',
195+
onClick: () {
196+
final toggle =
197+
web.document.getElementById('open-filter-toggle')
198+
as web.HTMLInputElement?;
199+
toggle?.checked = !toggle.checked;
200+
},
201+
[
202+
span(
203+
classes: 'material-symbols',
204+
attributes: {'aria-hidden': 'true', 'translate': 'no'},
205+
[text('filter_list')],
206+
),
207+
],
208+
),
209+
),
210+
]),
211+
div(classes: 'label-row', [
212+
label(
213+
attributes: {'for': 'resource-search'},
214+
[
215+
text('Showing '),
216+
span([text('$filteredResourcesCount')]),
217+
text(' / '),
218+
span([text('${resourceInfos.length}')]),
219+
],
220+
),
221+
button(
222+
attributes: {
223+
if (searchQuery.isEmpty &&
224+
filters.selectedTags.isEmpty &&
225+
filters.selectedTypes.isEmpty)
226+
'disabled': 'true',
227+
},
228+
onClick: () {
229+
setState(() {
230+
searchQuery = '';
231+
});
232+
filters.reset();
233+
},
234+
[
235+
span(
236+
classes: 'material-symbols',
237+
attributes: {'aria-hidden': 'true', 'translate': 'no'},
238+
[text('close_small')],
239+
),
240+
span([text('Clear filters')]),
241+
],
242+
),
243+
]),
244+
]);
245+
}
246+
}

0 commit comments

Comments
 (0)