Skip to content

Commit

Permalink
1.11.2 - Adding shimmer effect (#206)
Browse files Browse the repository at this point in the history
* adding shimmer effect when loading search results

* adding shimmer to the board game images

* fixing plays history not refreshing automatically

* small fixes for the details page if general info is null

* Adding health check to the search API

* adding timout search timeout and showing an appropriate error for it
  • Loading branch information
mkieres authored Jul 9, 2023
1 parent 1d8a032 commit ff76cf8
Show file tree
Hide file tree
Showing 18 changed files with 649 additions and 386 deletions.
2 changes: 2 additions & 0 deletions backend/BGC.SearchApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
.ValidateDataAnnotations()
.ValidateOnStart();

builder.Services.AddHealthChecks();
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.Configure<TelemetryConfiguration>(config =>
{
Expand Down Expand Up @@ -78,6 +79,7 @@
await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode)
.ExecuteAsync(statusCodeContext.HttpContext);
});
app.MapHealthChecks("/health");

app.MapGet("api/search", [Authorize] ([FromQuery] string query, ISearchService searchService) => searchService.Search(query, CancellationToken.None))
.WithOpenApi();
Expand Down
23 changes: 17 additions & 6 deletions board_games_companion/lib/common/app_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,18 @@ class AppText {
static const aboutPageCommunityJoinDiscord =
"Tap on the below logo to join the BGC's Discord server.";

static const boardGameDetailsPaboutGeneralTitle = 'General';
static const boardGameDetailsPaboutLinksTitle = 'Links';
static const boardGameDetailsPaboutCreditsTitle = 'Credits';
static const boardGameDetailsPaboutCategoriesTitle = 'Categories';
static const boardGameDetailsPaboutDescriptionTitle = 'Description';
static const boardGameDetailsPaboutCollectionsTitle = 'Collections';
static const boardGameDetailsPageGeneralTitle = 'General';
static const boardGameDetailsPagetLinksTitle = 'Links';
static const boardGameDetailsPageCreditsTitle = 'Credits';
static const boardGameDetailsPageCategoriesTitle = 'Categories';
static const boardGameDetailsPageDescriptionTitle = 'Description';
static const boardGameDetailsPageCollectionsTitle = 'Collections';

static const boardGameDetailsPaboutGameNotRanked = 'Not ranked';
static const boardGameDetailsPaboutGameNotRated = 'Not rated';
static const boardGameDetailsPaboutGameRatingFormat = '%s ratings';
static const boardGameDetailsPaboutGameNoComments = 'No comments';
static const boardGameDetailsPaboutGameCommentsFormat = '%s comments';

static const hotBoardGamesPageTitle = 'Hot Board Games';

Expand Down Expand Up @@ -113,6 +119,11 @@ class AppText {
static const searchBoardGamesSearchRetry = 'Retry';
static const searchBoardGamesCreateGame = 'Create game';

static const searchBoardGamesErrorTitle = 'Sorry, we ran into a problem';
static const searchBoardGamesTimeoutError =
'Unfortunately our services timed out. Please try searching again.';
static const searchBoardGamesGenericError = 'Check your internet connectivity and try again.';

static const gamesPageMainGamesSliverSectionTitleFormat = 'Main Games (%s)';
static const gamesPageExpansionsSliverSectionTitleFormat = '%s Expansions (%s)';

Expand Down
23 changes: 18 additions & 5 deletions board_games_companion/lib/models/hive/board_game_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,33 @@ class BoardGameDetails with _$BoardGameDetails {
return sprintf(AppText.gamePlayersRangeFormat, [minPlayers, maxPlayers]);
}

String? get rankFormatted {
String get rankFormatted {
if (rank != null) {
return rank.toString();
} else if (ranks.isNotEmpty) {
final overallRank = ranks.first.rank;
return overallRank?.toString() ?? 'Not Ranked';
return overallRank?.toString() ?? AppText.boardGameDetailsPaboutGameNotRanked;
}

return null;
return AppText.boardGameDetailsPaboutGameNotRanked;
}

String? get votesFormatted => _formatNumber(votes);
String get votesNumberFormatted {
if (votes == null) {
return AppText.boardGameDetailsPaboutGameNotRated;
}

return sprintf(AppText.boardGameDetailsPaboutGameRatingFormat, [_formatNumber(votes)]);
}

String? get commentsNumberFormatted => _formatNumber(commentsNumber);
String get commentsNumberFormatted {
if (votes == null) {
return AppText.boardGameDetailsPaboutGameNoComments;
}

return sprintf(
AppText.boardGameDetailsPaboutGameCommentsFormat, [_formatNumber(commentsNumber)]);
}

bool get hasIncompleteDetails =>
(isBggSynced ?? false) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@ We couldn't retrieve any board games. Check your Internet connectivity and try a
delegate: SliverChildListDelegate.fixed(
<Widget>[
SectionHeader.titles(
primaryTitle: AppText.boardGameDetailsPaboutGeneralTitle,
secondaryTitle: AppText.boardGameDetailsPaboutCollectionsTitle,
primaryTitle: AppText.boardGameDetailsPageGeneralTitle,
secondaryTitle: AppText.boardGameDetailsPageCollectionsTitle,
),
const SizedBox(height: _sectionTopSpacing),
_GeneralAndCollections(viewModel: viewModel),
Expand All @@ -260,15 +260,15 @@ We couldn't retrieve any board games. Check your Internet connectivity and try a
),
const SizedBox(height: _halfSpacingBetweenSecions),
if (!viewModel.isCreatedByUser) ...[
SectionHeader.title(title: AppText.boardGameDetailsPaboutLinksTitle),
SectionHeader.title(title: AppText.boardGameDetailsPagetLinksTitle),
_Links(boardGameDetailsStore: viewModel),
const SizedBox(height: _halfSpacingBetweenSecions),
SectionHeader.title(title: AppText.boardGameDetailsPaboutCreditsTitle),
SectionHeader.title(title: AppText.boardGameDetailsPageCreditsTitle),
const SizedBox(height: _sectionTopSpacing),
_Credits(boardGameDetails: viewModel.boardGame),
const SizedBox(height: _halfSpacingBetweenSecions),
SectionHeader.title(
title: AppText.boardGameDetailsPaboutCategoriesTitle,
title: AppText.boardGameDetailsPageCategoriesTitle,
),
_Categories(categories: viewModel.boardGame.categories!),
if (viewModel.isMainGame && viewModel.hasExpansions) ...[
Expand All @@ -286,7 +286,7 @@ We couldn't retrieve any board games. Check your Internet connectivity and try a
const SizedBox(height: _halfSpacingBetweenSecions),
],
SectionHeader.title(
title: AppText.boardGameDetailsPaboutDescriptionTitle,
title: AppText.boardGameDetailsPageDescriptionTitle,
),
const SizedBox(height: _sectionTopSpacing),
Padding(
Expand Down Expand Up @@ -607,21 +607,21 @@ class _GeneralAndCollections extends StatelessWidget {
BoardGameProperty(
icon: const Icon(Icons.tag, size: _iconSize),
iconWidth: _iconSize,
propertyName: '${viewModel.boardGame.rankFormatted}',
propertyName: viewModel.boardGame.rankFormatted,
fontSize: Dimensions.mediumFontSize,
),
const SizedBox(height: Dimensions.halfStandardSpacing),
BoardGameProperty(
icon: const Icon(Icons.how_to_vote, size: _iconSize),
iconWidth: _iconSize,
propertyName: '${viewModel.boardGame.votesFormatted} ratings',
propertyName: viewModel.boardGame.votesNumberFormatted,
fontSize: Dimensions.mediumFontSize,
),
const SizedBox(height: Dimensions.halfStandardSpacing),
BoardGameProperty(
icon: const Icon(Icons.comment, size: _iconSize),
iconWidth: _iconSize,
propertyName: '${viewModel.boardGame.commentsNumberFormatted} comments',
propertyName: viewModel.boardGame.commentsNumberFormatted,
fontSize: Dimensions.mediumFontSize,
),
const SizedBox(height: Dimensions.halfStandardSpacing),
Expand Down Expand Up @@ -717,14 +717,14 @@ class _SecondRowGeneralInfoPanels extends StatelessWidget {
),
),
const SizedBox(width: Dimensions.standardSpacing),
if (avgWeight != null)
if (avgWeight != null && avgWeight != 0)
Expanded(
child: _InfoPanel(
icon: const FaIcon(FontAwesomeIcons.scaleUnbalanced),
title: '${avgWeight!.toStringAsFixed(2)} / 5',
),
),
if (avgWeight == null) const Spacer()
if (avgWeight == null || avgWeight == 0) const Spacer()
],
),
),
Expand Down
54 changes: 34 additions & 20 deletions board_games_companion/lib/pages/home/home_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import 'dart:async';

import 'package:async/async.dart';
import 'package:board_games_companion/models/api/search/board_game_search_dto.dart';
import 'package:board_games_companion/services/board_games_search_service.dart';
import 'package:board_games_companion/widgets/search/board_game_search_error.dart';
import 'package:collection/collection.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -84,6 +87,8 @@ abstract class _HomeViewModelBase with Store {
String? _searchQuery;
String? _previousSearchQuery;

CancelableOperation<List<BoardGameSearchResultDto>>? _searchBoardGamesOperation;

@observable
ObservableStream<List<BoardGameDetails>> searchResultsStream = ObservableStream(Stream.value([]));

Expand Down Expand Up @@ -177,6 +182,8 @@ abstract class _HomeViewModelBase with Store {
return;
}

await _searchBoardGamesOperation?.cancel();

if (_previousSearchQuery == _searchQuery && (searchResultsStream.value?.isNotEmpty ?? false)) {
// Refresh data in the stream, otherwise the [ConnectionState] won't change from waiting
_searchResultsStreamController.add(searchResultsStream.value!);
Expand All @@ -185,29 +192,36 @@ abstract class _HomeViewModelBase with Store {

await _captureSearchDetailsEvent(_searchQuery!);

try {
_searchResults.clear();
final searchResultBoardGames = await _boardGameSearchServive.search(_searchQuery);
for (final searchResultBoardGame in searchResultBoardGames) {
// Enrich game details, if game details are available.
// Otherwise add the game to the store.
final boardGameDetails = BoardGameDetails.fromSearchResult(searchResultBoardGame);
if (_boardGamesStore.allBoardGamesMap.containsKey(boardGameDetails.id)) {
_searchResults.add(_boardGamesStore.allBoardGamesMap[boardGameDetails.id]!);
continue;
}

await _boardGamesStore.addOrUpdateBoardGame(boardGameDetails);
_searchResults.add(boardGameDetails);
_searchResults.clear();
_searchBoardGamesOperation = CancelableOperation<List<BoardGameSearchResultDto>>.fromFuture(
_boardGameSearchServive.search(_searchQuery));
_searchBoardGamesOperation!.value.onError<Exception>((error, stackTrace) {
FirebaseCrashlytics.instance.recordError(error, stackTrace);
if (error is TimeoutException) {
_searchResultsStreamController.addError(const BoardGameSearchError.timout());
} else {
_searchResultsStreamController.addError(const BoardGameSearchError.generic());
}

_previousSearchQuery = _searchQuery;
_searchResultsStreamController.add(_searchResults..sortBy(searchSelectedSortBy));
} catch (e, stack) {
FirebaseCrashlytics.instance.recordError(e, stack);
_searchResultsStreamController.addError(e);
rethrow;
return Future.value([]);
});

final searchResultBoardGames = await _searchBoardGamesOperation!.value;
for (final searchResultBoardGame in searchResultBoardGames) {
// Enrich game details, if game details are available.
// Otherwise add the game to the store.
final boardGameDetails = BoardGameDetails.fromSearchResult(searchResultBoardGame);
if (_boardGamesStore.allBoardGamesMap.containsKey(boardGameDetails.id)) {
_searchResults.add(_boardGamesStore.allBoardGamesMap[boardGameDetails.id]!);
continue;
}

await _boardGamesStore.addOrUpdateBoardGame(boardGameDetails);
_searchResults.add(boardGameDetails);
}

_previousSearchQuery = _searchQuery;
_searchResultsStreamController.add(_searchResults..sortBy(searchSelectedSortBy));
}

Future<void> _captureSearchDetailsEvent(String query) async {
Expand Down
43 changes: 30 additions & 13 deletions board_games_companion/lib/pages/plays/plays_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,15 @@ class _PlaysPageState extends State<PlaysPage> with SingleTickerProviderStateMix
Observer(
builder: (_) {
return widget.viewModel.visualState?.when(
history: (tab, historicalPlaythroughs) {
if (historicalPlaythroughs.isEmpty) {
return const _NoPlaythroughsSliver();
} else {
return _HistoricalPlaythroughSliverList(
historicalPlaythroughs: historicalPlaythroughs,
history: () => Observer(
builder: (_) {
return _HistoryTab(
historicalPlaythroughs: widget.viewModel.historicalPlaythroughs,
);
}
},
statistics: (tab) => const SliverToBoxAdapter(),
selectGame: (tab, shuffledBoardGames) {
},
),
statistics: () => const SliverToBoxAdapter(),
selectGame: () {
if (!widget.viewModel.hasAnyBoardGames) {
return const _NoBoardGamesSliver();
}
Expand All @@ -146,7 +144,7 @@ class _PlaysPageState extends State<PlaysPage> with SingleTickerProviderStateMix
if (widget.viewModel.hasAnyBoardGamesToShuffle)
_GameSpinnerSliver(
scrollController: _scrollController,
shuffledBoardGames: shuffledBoardGames,
shuffledBoardGames: widget.viewModel.shuffledBoardGames,
onSpin: () => _spin(),
onGameSelected: () => _selectGame(),
),
Expand Down Expand Up @@ -1214,7 +1212,7 @@ class _AppBar extends StatelessWidget {
AppBarBottomTab(
AppText.playsPageHistoryTabTitle,
Icons.history,
isSelected: tabVisualState?.playsTab == PlaysTab.history,
isSelected: tabVisualState == const PlaysPageVisualState.history(),
),
// TODO Add stats page
// AppBarBottomTab(
Expand All @@ -1225,7 +1223,7 @@ class _AppBar extends StatelessWidget {
AppBarBottomTab(
AppText.playsPageSelectGameTabTitle,
Icons.shuffle,
isSelected: tabVisualState?.playsTab == PlaysTab.selectGame,
isSelected: tabVisualState == const PlaysPageVisualState.selectGame(),
),
],
indicatorColor: AppColors.accentColor,
Expand All @@ -1236,6 +1234,25 @@ class _AppBar extends StatelessWidget {
}
}

class _HistoryTab extends StatelessWidget {
const _HistoryTab({
required this.historicalPlaythroughs,
});

final List<HistoricalPlaythrough> historicalPlaythroughs;

@override
Widget build(BuildContext context) {
if (historicalPlaythroughs.isEmpty) {
return const _NoPlaythroughsSliver();
} else {
return _HistoricalPlaythroughSliverList(
historicalPlaythroughs: historicalPlaythroughs,
);
}
}
}

class _FilterPlaytime extends BgcSegmentedButton<PlaytimeFilter> {
_FilterPlaytime.time({
required bool isSelected,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import 'package:board_games_companion/pages/plays/historical_playthrough.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

import '../../common/enums/plays_tab.dart';
import '../../models/hive/board_game_details.dart';

part 'plays_page_visual_states.freezed.dart';

@freezed
class PlaysPageVisualState with _$PlaysPageVisualState {
const factory PlaysPageVisualState.history(
PlaysTab playsTab,
List<HistoricalPlaythrough> historicalPlaythroughs,
) = _History;
const factory PlaysPageVisualState.statistics(PlaysTab playsTab) = _Statistics;
const factory PlaysPageVisualState.selectGame(
PlaysTab playsTab,
List<BoardGameDetails> shuffledBoardGames,
) = _SelectGame;
const factory PlaysPageVisualState.history() = _History;
const factory PlaysPageVisualState.statistics() = _Statistics;
const factory PlaysPageVisualState.selectGame() = _SelectGame;
}
Loading

0 comments on commit ff76cf8

Please sign in to comment.