Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions site/lib/jaspr_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,25 @@ import 'package:docs_flutter_dev_site/src/components/client/dartpad_injector.dar
as prefix2;
import 'package:docs_flutter_dev_site/src/components/client/download_latest_button.dart'
as prefix3;
import 'package:docs_flutter_dev_site/src/components/client/on_this_page_button.dart'
import 'package:docs_flutter_dev_site/src/components/client/learning_resource_filters.dart'
as prefix4;
import 'package:docs_flutter_dev_site/src/components/header/menu_toggle.dart'
import 'package:docs_flutter_dev_site/src/components/client/learning_resource_filters_sidebar.dart'
as prefix5;
import 'package:docs_flutter_dev_site/src/components/header/site_switcher.dart'
import 'package:docs_flutter_dev_site/src/components/client/on_this_page_button.dart'
as prefix6;
import 'package:docs_flutter_dev_site/src/components/header/theme_switcher.dart'
import 'package:docs_flutter_dev_site/src/components/header/menu_toggle.dart'
as prefix7;
import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart'
import 'package:docs_flutter_dev_site/src/components/header/site_switcher.dart'
as prefix8;
import 'package:docs_flutter_dev_site/src/components/copy_button.dart'
import 'package:docs_flutter_dev_site/src/components/header/theme_switcher.dart'
as prefix9;
import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix10;
import 'package:docs_flutter_dev_site/src/components/os_selector.dart'
import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart'
as prefix10;
import 'package:docs_flutter_dev_site/src/components/copy_button.dart'
as prefix11;
import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix12;
import 'package:docs_flutter_dev_site/src/components/os_selector.dart'
as prefix13;

/// Default [JasprOptions] for use with your jaspr project.
///
Expand Down Expand Up @@ -66,37 +70,47 @@ JasprOptions get defaultJasprOptions => JasprOptions(
params: _prefix3DownloadLatestButton,
),

prefix4.OnThisPageButton: ClientTarget<prefix4.OnThisPageButton>(
prefix4.LearningResourceFilters:
ClientTarget<prefix4.LearningResourceFilters>(
'src/components/client/learning_resource_filters',
),

prefix5.LearningResourceFiltersSidebar:
ClientTarget<prefix5.LearningResourceFiltersSidebar>(
'src/components/client/learning_resource_filters_sidebar',
),

prefix6.OnThisPageButton: ClientTarget<prefix6.OnThisPageButton>(
'src/components/client/on_this_page_button',
),

prefix8.CookieNotice: ClientTarget<prefix8.CookieNotice>(
prefix10.CookieNotice: ClientTarget<prefix10.CookieNotice>(
'src/components/cookie_notice',
),

prefix9.CopyButton: ClientTarget<prefix9.CopyButton>(
prefix11.CopyButton: ClientTarget<prefix11.CopyButton>(
'src/components/copy_button',
params: _prefix9CopyButton,
params: _prefix11CopyButton,
),

prefix10.FeedbackComponent: ClientTarget<prefix10.FeedbackComponent>(
prefix12.FeedbackComponent: ClientTarget<prefix12.FeedbackComponent>(
'src/components/feedback',
params: _prefix10FeedbackComponent,
params: _prefix12FeedbackComponent,
),

prefix5.MenuToggle: ClientTarget<prefix5.MenuToggle>(
prefix7.MenuToggle: ClientTarget<prefix7.MenuToggle>(
'src/components/header/menu_toggle',
),

prefix6.SiteSwitcher: ClientTarget<prefix6.SiteSwitcher>(
prefix8.SiteSwitcher: ClientTarget<prefix8.SiteSwitcher>(
'src/components/header/site_switcher',
),

prefix7.ThemeSwitcher: ClientTarget<prefix7.ThemeSwitcher>(
prefix9.ThemeSwitcher: ClientTarget<prefix9.ThemeSwitcher>(
'src/components/header/theme_switcher',
),

prefix11.OsSelector: ClientTarget<prefix11.OsSelector>(
prefix13.OsSelector: ClientTarget<prefix13.OsSelector>(
'src/components/os_selector',
),
},
Expand All @@ -116,11 +130,11 @@ Map<String, dynamic> _prefix2DartPadInjector(prefix2.DartPadInjector c) => {
Map<String, dynamic> _prefix3DownloadLatestButton(
prefix3.DownloadLatestButton c,
) => {'os': c.os, 'arch': c.arch};
Map<String, dynamic> _prefix9CopyButton(prefix9.CopyButton c) => {
Map<String, dynamic> _prefix11CopyButton(prefix11.CopyButton c) => {
'toCopy': c.toCopy,
'buttonText': c.buttonText,
'classes': c.classes,
'title': c.title,
};
Map<String, dynamic> _prefix10FeedbackComponent(prefix10.FeedbackComponent c) =>
Map<String, dynamic> _prefix12FeedbackComponent(prefix12.FeedbackComponent c) =>
{'issueUrl': c.issueUrl};
3 changes: 1 addition & 2 deletions site/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import 'src/components/expansion_list.dart';
import 'src/components/os_selector.dart';
import 'src/components/pages/learning_resource_index.dart';
import 'src/components/tabs.dart';
import 'src/data/learning_resources.dart';
import 'src/extensions/registry.dart';
import 'src/layouts/catalog_page_layout.dart';
import 'src/layouts/doc_layout.dart';
Expand Down Expand Up @@ -127,7 +126,7 @@ List<CustomComponent> get _embeddableComponents => [
),
CustomComponent(
pattern: RegExp('LearningResourceIndex', caseSensitive: false),
builder: (_, _, _) => LearningResourceIndex(allLearningResources),
builder: (_, _, _) => LearningResourceIndex(),
),
CustomComponent(
pattern: RegExp('ArchiveTable'),
Expand Down
4 changes: 4 additions & 0 deletions site/lib/src/analytics/analytics_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:meta/meta.dart';
import 'package:universal_web/js_interop.dart';
import 'package:universal_web/web.dart' as web;

import '../util.dart';
import 'analytics.dart';

/// Web implementation of [Analytics].
Expand All @@ -15,6 +16,9 @@ import 'analytics.dart';
final class AnalyticsImplementation extends Analytics {
@override
void sendEvent(String eventName, Map<String, Object?> parameters) {
if (!productionBuild) {
return;
}
final dataLayer = web.window['dataLayer'];
if (dataLayer.isA<JSArray>()) {
(dataLayer as JSArray).toDart.add(
Expand Down
207 changes: 207 additions & 0 deletions site/lib/src/components/client/learning_resource_filters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright 2025 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math';

import 'package:jaspr/jaspr.dart';
import 'package:universal_web/js_interop.dart';
import 'package:universal_web/web.dart' as web;

import '../../analytics/analytics.dart';
import '../../models/learning_resource_model.dart';
import '../util/global_event_listener.dart';
import 'learning_resource_filters_sidebar.dart';

@client
class LearningResourceFilters extends StatefulComponent {
const LearningResourceFilters({super.key});

@override
State<LearningResourceFilters> createState() =>
_LearningResourceFiltersState();
}

class _LearningResourceFiltersState extends State<LearningResourceFilters> {
String searchQuery = '';

FiltersNotifier get filters => LearningResourceFiltersSidebar.filters;

final List<LearningResource> resources = [];
int filteredResourcesCount = 0;

@override
void initState() {
super.initState();

if (kIsWeb) {
filters.addListener(setFilters);

final resourceGrid = web.document.getElementById('all-resources-grid');
if (resourceGrid == null) {
return;
}

final resourceCards = resourceGrid.querySelectorAll('.card');
recreateResources(resourceCards);
shuffleCards(resourceGrid);
}
}

void recreateResources(web.NodeList resourceCards) {
for (var i = 0; i < resourceCards.length; i++) {
final element = resourceCards.item(i) as web.Element;
final info = LearningResource.fromElement(element);
resources.add(info);

element.addEventListener(
'click',
((web.Event event) {
analytics.sendEvent('learning_resource_index_click', {
'learning_resource_type': info.type,
'learning_resource_title': info.name,
});
}).toJS,
);
}
filteredResourcesCount = resources.length;
}

void shuffleCards(web.Element container) {
final r = Random();
final elements = container.childNodes;
for (var i = elements.length; i > 0; i--) {
final card = elements.item(r.nextInt(i));
container.appendChild(card!);
}
}

/// Update the filter state and re-evaluate which resources to show.
///
/// Use like the `setState` method by passing a callback that updates
/// the relevant state variables.
///
/// Example:
///
/// ```dart
/// setFilters(() {
/// searchQuery = '...';
/// });
/// ```
void setFilters([void Function()? callback]) {
setState(callback ?? () {});

final resourcesToShow = filters.filterResources(resources, searchQuery);
filteredResourcesCount = resourcesToShow.length;
for (final info in resources) {
final element =
web.document.getElementById(info.name) as web.HTMLElement?;
if (element == null) {
continue;
}

if (resourcesToShow.contains(info)) {
element.classList.remove('hidden');
} else {
element.classList.add('hidden');
}
}
}

@override
void dispose() {
if (kIsWeb) {
filters.removeListener(setFilters);
}
super.dispose();
}

@override
Component build(BuildContext context) {
return div(id: 'resource-search-group', classes: 'chip-filters-group', [
div(classes: 'top-row', [
div(classes: 'search-wrapper', id: 'resource-search', [
span(
classes: 'material-symbols leading-icon',
attributes: {'aria-hidden': 'true', 'translate': 'no'},
[text('search')],
),
input(
type: InputType.search,
attributes: {
'placeholder': 'Try "button" or "networking"...',
'aria-label': 'Search learning resources by name and category',
},
value: searchQuery,
onInput: (value) {
setFilters(() {
searchQuery = value as String;
});
},
),
]),
GlobalEventListener(
onClick: (event) {
final target = event.target as web.Element?;
// If clicking outside the filters or toggle, close the filters.
if (target?.closest('#resource-filter-group-wrapper') == null &&
target?.closest('.show-filters-button') == null) {
final toggle =
web.document.getElementById('open-filter-toggle')
as web.HTMLInputElement?;
toggle?.checked = false;
}
},
button(
classes: 'icon-button show-filters-button',
onClick: () {
final toggle =
web.document.getElementById('open-filter-toggle')
as web.HTMLInputElement?;
toggle?.checked = !toggle.checked;
},
[
span(
classes: 'material-symbols',
attributes: {'aria-hidden': 'true', 'translate': 'no'},
[text('filter_list')],
),
],
),
),
]),
div(classes: 'label-row', [
label(
attributes: {'for': 'resource-search'},
[
text('Showing '),
span([text('$filteredResourcesCount')]),
text(' / '),
span([text('${resources.length}')]),
],
),
button(
attributes: {
if (searchQuery.isEmpty &&
filters.selectedTags.isEmpty &&
filters.selectedTypes.isEmpty)
'disabled': 'true',
},
onClick: () {
// No setState needed, since resetting filters will trigger it.
searchQuery = '';
filters.reset();
},
[
span(
classes: 'material-symbols',
attributes: {'aria-hidden': 'true', 'translate': 'no'},
[text('close_small')],
),
span([text('Clear filters')]),
],
),
]),
]);
}
}
Loading