diff --git a/data/lib/api/tournament/tournament_model.dart b/data/lib/api/tournament/tournament_model.dart index 568291b2..f7bcb841 100644 --- a/data/lib/api/tournament/tournament_model.dart +++ b/data/lib/api/tournament/tournament_model.dart @@ -34,6 +34,9 @@ class TournamentModel with _$TournamentModel { @JsonKey(includeFromJson: false, includeToJson: false) @Default([]) List matches, + @JsonKey(includeFromJson: false, includeToJson: false) + @Default([]) + List keyStats, }) = _TournamentModel; factory TournamentModel.fromJson(Map json) => @@ -52,7 +55,8 @@ class TournamentMember with _$TournamentMember { const factory TournamentMember({ required String id, @JsonKey(includeToJson: false, includeFromJson: false) - @Default(UserModel(id: '')) UserModel user, + @Default(UserModel(id: '')) + UserModel user, @Default(TournamentMemberRole.admin) TournamentMemberRole role, }) = _TournamentMember; @@ -85,3 +89,13 @@ enum TournamentMemberRole { bool get isAdmin => this == TournamentMemberRole.admin; } + +@freezed +class PlayerKeyStat with _$PlayerKeyStat { + @JsonSerializable(explicitToJson: true) + const factory PlayerKeyStat({ + required String teamName, + required UserModel player, + required int runs, + }) = _PlayerKeyStat; +} diff --git a/data/lib/api/tournament/tournament_model.freezed.dart b/data/lib/api/tournament/tournament_model.freezed.dart index ed37d162..2e3bc29e 100644 --- a/data/lib/api/tournament/tournament_model.freezed.dart +++ b/data/lib/api/tournament/tournament_model.freezed.dart @@ -39,6 +39,8 @@ mixin _$TournamentModel { List get teams => throw _privateConstructorUsedError; @JsonKey(includeFromJson: false, includeToJson: false) List get matches => throw _privateConstructorUsedError; + @JsonKey(includeFromJson: false, includeToJson: false) + List get keyStats => throw _privateConstructorUsedError; /// Serializes this TournamentModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -72,7 +74,9 @@ abstract class $TournamentModelCopyWith<$Res> { @JsonKey(includeFromJson: false, includeToJson: false) List teams, @JsonKey(includeFromJson: false, includeToJson: false) - List matches}); + List matches, + @JsonKey(includeFromJson: false, includeToJson: false) + List keyStats}); } /// @nodoc @@ -104,6 +108,7 @@ class _$TournamentModelCopyWithImpl<$Res, $Val extends TournamentModel> Object? match_ids = null, Object? teams = null, Object? matches = null, + Object? keyStats = null, }) { return _then(_value.copyWith( id: null == id @@ -162,6 +167,10 @@ class _$TournamentModelCopyWithImpl<$Res, $Val extends TournamentModel> ? _value.matches : matches // ignore: cast_nullable_to_non_nullable as List, + keyStats: null == keyStats + ? _value.keyStats + : keyStats // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -190,7 +199,9 @@ abstract class _$$TournamentModelImplCopyWith<$Res> @JsonKey(includeFromJson: false, includeToJson: false) List teams, @JsonKey(includeFromJson: false, includeToJson: false) - List matches}); + List matches, + @JsonKey(includeFromJson: false, includeToJson: false) + List keyStats}); } /// @nodoc @@ -220,6 +231,7 @@ class __$$TournamentModelImplCopyWithImpl<$Res> Object? match_ids = null, Object? teams = null, Object? matches = null, + Object? keyStats = null, }) { return _then(_$TournamentModelImpl( id: null == id @@ -278,6 +290,10 @@ class __$$TournamentModelImplCopyWithImpl<$Res> ? _value._matches : matches // ignore: cast_nullable_to_non_nullable as List, + keyStats: null == keyStats + ? _value._keyStats + : keyStats // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -302,12 +318,15 @@ class _$TournamentModelImpl implements _TournamentModel { @JsonKey(includeFromJson: false, includeToJson: false) final List teams = const [], @JsonKey(includeFromJson: false, includeToJson: false) - final List matches = const []}) + final List matches = const [], + @JsonKey(includeFromJson: false, includeToJson: false) + final List keyStats = const []}) : _members = members, _team_ids = team_ids, _match_ids = match_ids, _teams = teams, - _matches = matches; + _matches = matches, + _keyStats = keyStats; factory _$TournamentModelImpl.fromJson(Map json) => _$$TournamentModelImplFromJson(json); @@ -378,9 +397,18 @@ class _$TournamentModelImpl implements _TournamentModel { return EqualUnmodifiableListView(_matches); } + final List _keyStats; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + List get keyStats { + if (_keyStats is EqualUnmodifiableListView) return _keyStats; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_keyStats); + } + @override String toString() { - return 'TournamentModel(id: $id, name: $name, profile_img_url: $profile_img_url, banner_img_url: $banner_img_url, type: $type, members: $members, created_by: $created_by, created_at: $created_at, start_date: $start_date, end_date: $end_date, team_ids: $team_ids, match_ids: $match_ids, teams: $teams, matches: $matches)'; + return 'TournamentModel(id: $id, name: $name, profile_img_url: $profile_img_url, banner_img_url: $banner_img_url, type: $type, members: $members, created_by: $created_by, created_at: $created_at, start_date: $start_date, end_date: $end_date, team_ids: $team_ids, match_ids: $match_ids, teams: $teams, matches: $matches, keyStats: $keyStats)'; } @override @@ -408,7 +436,8 @@ class _$TournamentModelImpl implements _TournamentModel { const DeepCollectionEquality() .equals(other._match_ids, _match_ids) && const DeepCollectionEquality().equals(other._teams, _teams) && - const DeepCollectionEquality().equals(other._matches, _matches)); + const DeepCollectionEquality().equals(other._matches, _matches) && + const DeepCollectionEquality().equals(other._keyStats, _keyStats)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -428,7 +457,8 @@ class _$TournamentModelImpl implements _TournamentModel { const DeepCollectionEquality().hash(_team_ids), const DeepCollectionEquality().hash(_match_ids), const DeepCollectionEquality().hash(_teams), - const DeepCollectionEquality().hash(_matches)); + const DeepCollectionEquality().hash(_matches), + const DeepCollectionEquality().hash(_keyStats)); /// Create a copy of TournamentModel /// with the given fields replaced by the non-null parameter values. @@ -464,7 +494,9 @@ abstract class _TournamentModel implements TournamentModel { @JsonKey(includeFromJson: false, includeToJson: false) final List teams, @JsonKey(includeFromJson: false, includeToJson: false) - final List matches}) = _$TournamentModelImpl; + final List matches, + @JsonKey(includeFromJson: false, includeToJson: false) + final List keyStats}) = _$TournamentModelImpl; factory _TournamentModel.fromJson(Map json) = _$TournamentModelImpl.fromJson; @@ -502,6 +534,9 @@ abstract class _TournamentModel implements TournamentModel { @override @JsonKey(includeFromJson: false, includeToJson: false) List get matches; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + List get keyStats; /// Create a copy of TournamentModel /// with the given fields replaced by the non-null parameter values. @@ -726,3 +761,184 @@ abstract class _TournamentMember implements TournamentMember { _$$TournamentMemberImplCopyWith<_$TournamentMemberImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$PlayerKeyStat { + String get teamName => throw _privateConstructorUsedError; + UserModel get player => throw _privateConstructorUsedError; + int get runs => throw _privateConstructorUsedError; + + /// Create a copy of PlayerKeyStat + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PlayerKeyStatCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlayerKeyStatCopyWith<$Res> { + factory $PlayerKeyStatCopyWith( + PlayerKeyStat value, $Res Function(PlayerKeyStat) then) = + _$PlayerKeyStatCopyWithImpl<$Res, PlayerKeyStat>; + @useResult + $Res call({String teamName, UserModel player, int runs}); + + $UserModelCopyWith<$Res> get player; +} + +/// @nodoc +class _$PlayerKeyStatCopyWithImpl<$Res, $Val extends PlayerKeyStat> + implements $PlayerKeyStatCopyWith<$Res> { + _$PlayerKeyStatCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PlayerKeyStat + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? teamName = null, + Object? player = null, + Object? runs = null, + }) { + return _then(_value.copyWith( + teamName: null == teamName + ? _value.teamName + : teamName // ignore: cast_nullable_to_non_nullable + as String, + player: null == player + ? _value.player + : player // ignore: cast_nullable_to_non_nullable + as UserModel, + runs: null == runs + ? _value.runs + : runs // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } + + /// Create a copy of PlayerKeyStat + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserModelCopyWith<$Res> get player { + return $UserModelCopyWith<$Res>(_value.player, (value) { + return _then(_value.copyWith(player: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$PlayerKeyStatImplCopyWith<$Res> + implements $PlayerKeyStatCopyWith<$Res> { + factory _$$PlayerKeyStatImplCopyWith( + _$PlayerKeyStatImpl value, $Res Function(_$PlayerKeyStatImpl) then) = + __$$PlayerKeyStatImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String teamName, UserModel player, int runs}); + + @override + $UserModelCopyWith<$Res> get player; +} + +/// @nodoc +class __$$PlayerKeyStatImplCopyWithImpl<$Res> + extends _$PlayerKeyStatCopyWithImpl<$Res, _$PlayerKeyStatImpl> + implements _$$PlayerKeyStatImplCopyWith<$Res> { + __$$PlayerKeyStatImplCopyWithImpl( + _$PlayerKeyStatImpl _value, $Res Function(_$PlayerKeyStatImpl) _then) + : super(_value, _then); + + /// Create a copy of PlayerKeyStat + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? teamName = null, + Object? player = null, + Object? runs = null, + }) { + return _then(_$PlayerKeyStatImpl( + teamName: null == teamName + ? _value.teamName + : teamName // ignore: cast_nullable_to_non_nullable + as String, + player: null == player + ? _value.player + : player // ignore: cast_nullable_to_non_nullable + as UserModel, + runs: null == runs + ? _value.runs + : runs // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true) +class _$PlayerKeyStatImpl implements _PlayerKeyStat { + const _$PlayerKeyStatImpl( + {required this.teamName, required this.player, required this.runs}); + + @override + final String teamName; + @override + final UserModel player; + @override + final int runs; + + @override + String toString() { + return 'PlayerKeyStat(teamName: $teamName, player: $player, runs: $runs)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlayerKeyStatImpl && + (identical(other.teamName, teamName) || + other.teamName == teamName) && + (identical(other.player, player) || other.player == player) && + (identical(other.runs, runs) || other.runs == runs)); + } + + @override + int get hashCode => Object.hash(runtimeType, teamName, player, runs); + + /// Create a copy of PlayerKeyStat + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PlayerKeyStatImplCopyWith<_$PlayerKeyStatImpl> get copyWith => + __$$PlayerKeyStatImplCopyWithImpl<_$PlayerKeyStatImpl>(this, _$identity); +} + +abstract class _PlayerKeyStat implements PlayerKeyStat { + const factory _PlayerKeyStat( + {required final String teamName, + required final UserModel player, + required final int runs}) = _$PlayerKeyStatImpl; + + @override + String get teamName; + @override + UserModel get player; + @override + int get runs; + + /// Create a copy of PlayerKeyStat + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PlayerKeyStatImplCopyWith<_$PlayerKeyStatImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/data/lib/api/tournament/tournament_model.g.dart b/data/lib/api/tournament/tournament_model.g.dart index 475a8cd0..de9022c5 100644 --- a/data/lib/api/tournament/tournament_model.g.dart +++ b/data/lib/api/tournament/tournament_model.g.dart @@ -97,3 +97,17 @@ const _$TournamentMemberRoleEnumMap = { TournamentMemberRole.organizer: 'organizer', TournamentMemberRole.admin: 'admin', }; + +_$PlayerKeyStatImpl _$$PlayerKeyStatImplFromJson(Map json) => + _$PlayerKeyStatImpl( + teamName: json['teamName'] as String, + player: UserModel.fromJson(json['player'] as Map), + runs: (json['runs'] as num).toInt(), + ); + +Map _$$PlayerKeyStatImplToJson(_$PlayerKeyStatImpl instance) => + { + 'teamName': instance.teamName, + 'player': instance.player.toJson(), + 'runs': instance.runs, + }; diff --git a/data/lib/service/ball_score/ball_score_service.dart b/data/lib/service/ball_score/ball_score_service.dart index 144920ea..3a754e90 100644 --- a/data/lib/service/ball_score/ball_score_service.dart +++ b/data/lib/service/ball_score/ball_score_service.dart @@ -182,6 +182,27 @@ class BallScoreService { throw AppError.fromError(error, stack); } } + + Future getPlayerTotalRuns(String matchId, String playerId) async { + try { + final filter = Filter.and( + Filter(FireStoreConst.matchId, isEqualTo: matchId), + Filter(FireStoreConst.batsmanId, isEqualTo: playerId), + ); + + final snapshot = await _ballScoreCollection.where(filter).get(); + final totalRuns = snapshot.docs.fold( + 0, + (total, doc) { + final runsScored = doc.data().runs_scored; + return runsScored > 0 ? total + runsScored : total; + }, + ); + return totalRuns; + } catch (error, stack) { + throw AppError.fromError(error, stack); + } + } } class BallScoreChange { diff --git a/data/lib/service/match/match_service.dart b/data/lib/service/match/match_service.dart index 1a0b0a9b..deda9c7a 100644 --- a/data/lib/service/match/match_service.dart +++ b/data/lib/service/match/match_service.dart @@ -577,10 +577,13 @@ class MatchService { Future> getMatchesByIds(List matchIds) async { try { - return await _matchCollection - .where(FieldPath.documentId, whereIn: matchIds) - .get() - .then((value) => value.docs.map((e) => e.data()).toList()); + final List matches = []; + for (var matchId in matchIds) { + final match = await getMatchById(matchId); + matches.add(match); + } + + return matches; } catch (error, stack) { throw AppError.fromError(error, stack); } diff --git a/data/lib/service/tournament/tournament_service.dart b/data/lib/service/tournament/tournament_service.dart index f45076cc..c252a5dc 100644 --- a/data/lib/service/tournament/tournament_service.dart +++ b/data/lib/service/tournament/tournament_service.dart @@ -1,9 +1,11 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../api/match/match_model.dart'; import '../../api/tournament/tournament_model.dart'; import '../../errors/app_error.dart'; import '../../utils/constant/firestore_constant.dart'; +import '../ball_score/ball_score_service.dart'; import '../match/match_service.dart'; import '../team/team_service.dart'; import '../user/user_service.dart'; @@ -14,6 +16,7 @@ final tournamentServiceProvider = Provider( ref.read(teamServiceProvider), ref.read(matchServiceProvider), ref.read(userServiceProvider), + ref.read(ballScoreServiceProvider), ), ); @@ -22,12 +25,14 @@ class TournamentService { final TeamService _teamService; final MatchService _matchService; final UserService _userService; + final BallScoreService _ballScoreService; TournamentService( this._firestore, this._teamService, this._matchService, this._userService, + this._ballScoreService, ); CollectionReference get _tournamentCollection => @@ -92,7 +97,9 @@ class TournamentService { if (matchIds.isNotEmpty) { final matches = await _matchService.getMatchesByIds(matchIds); - tournament = tournament.copyWith(matches: matches); + final keyStats = await getKeyStats(matches); + tournament = + tournament.copyWith(matches: matches, keyStats: keyStats); } if (tournament.members.isNotEmpty) { @@ -111,4 +118,34 @@ class TournamentService { } }).handleError((error, stack) => throw AppError.fromError(error, stack)); } + + Future> getKeyStats(List matches) async { + final Map playerStatsMap = {}; + + for (var match in matches) { + for (var team in match.teams) { + for (var player in team.squad) { + final totalRuns = + await _ballScoreService.getPlayerTotalRuns(match.id, player.id); + + if (totalRuns > 0) { + if (playerStatsMap.containsKey(player.id)) { + playerStatsMap[player.id] = playerStatsMap[player.id]!.copyWith( + runs: playerStatsMap[player.id]!.runs + totalRuns, + ); + } else { + playerStatsMap[player.id] = PlayerKeyStat( + teamName: team.team.name, + player: player.player, + runs: totalRuns, + ); + } + } + } + } + } + + return playerStatsMap.values.toList() + ..sort((a, b) => b.runs.compareTo(a.runs)); + } } diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index 2c11657b..2811d3ca 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -59,6 +59,7 @@ "common_second_inning_title": "Second inning", "common_qr_code_title": "QR Code", "common_verify_title": "Verify", + "common_view_all": "View all", "common_obscure_phone_number_text": "{countryCode} ***** ***{lastDigits}", "@common_obscure_phone_number_text": { "description": "+{countryCode} ***** ***{lastDigits}", @@ -118,7 +119,6 @@ "home_screen_create_match_btn": "Create match", "home_screen_set_up_team_title": "Set up a team in minutes.", "home_screen_create_team_btn": "Create team", - "home_screen_view_all_btn": "View all", "home_screen_no_matches_title": "No Matches in this area!", "home_screen_no_matches_description_text": "Enjoy the freedom of creating your own cricket matches or teams.", @@ -183,6 +183,14 @@ "tournament_detail_points_table_tab": "Points Table", "tournament_detail_stats_tab": "Stats", + "tournament_detail_overview_info_title": "Tournament info", + "tournament_detail_overview_type_title": "Type", + "tournament_detail_overview_duration_title": "Duration", + "tournament_detail_overview_teams_squads_title": "Teams Squads", + "tournament_detail_overview_key_stats_title": "Key Stats", + "tournament_detail_overview_key_stats_most_runs_title": "Most Runs", + "tournament_detail_overview_featured_matches_title": "Featured Matches", + "@_TOURNAMENT_TYPE":{ }, "tournament_type_knock_out": "Knockout", diff --git a/khelo/lib/components/won_by_message_text.dart b/khelo/lib/components/won_by_message_text.dart index b65b1041..4b6c8586 100644 --- a/khelo/lib/components/won_by_message_text.dart +++ b/khelo/lib/components/won_by_message_text.dart @@ -2,17 +2,20 @@ import 'package:data/api/match/match_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/extensions/enum_extensions.dart'; +import 'package:khelo/domain/extensions/string_extensions.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; class WonByMessageText extends StatelessWidget { final MatchResult? matchResult; final TextStyle? textStyle; + final bool isTournament; const WonByMessageText({ super.key, this.matchResult, this.textStyle, + this.isTournament = false, }); @override @@ -26,21 +29,42 @@ class WonByMessageText extends StatelessWidget { AppTextStyle.subtitle1 .copyWith(color: context.colorScheme.textPrimary)); } - return Text.rich(TextSpan( - text: matchResult!.teamName, - style: textStyle ?? - AppTextStyle.subtitle2 - .copyWith(color: context.colorScheme.textPrimary), + + if (isTournament) { + return Column( children: [ - TextSpan( - text: context.l10n.score_board_won_by_title, - style: textStyle ?? - AppTextStyle.body2 - .copyWith(color: context.colorScheme.textDisabled)), - TextSpan( - text: matchResult!.winType - .getString(context, matchResult!.difference), + Text( + '${matchResult!.teamName.initials(limit: 2)} Won', + style: textStyle ?? + AppTextStyle.subtitle2 + .copyWith(color: context.colorScheme.textPrimary), + ), + Text( + 'by ${matchResult!.winType.getString(context, matchResult!.difference)}', + style: textStyle ?? + AppTextStyle.caption.copyWith( + color: context.colorScheme.textDisabled, + ), ), - ])); + ], + ); + } else { + return Text.rich(TextSpan( + text: matchResult!.teamName, + style: textStyle ?? + AppTextStyle.subtitle2 + .copyWith(color: context.colorScheme.textPrimary), + children: [ + TextSpan( + text: context.l10n.score_board_won_by_title, + style: textStyle ?? + AppTextStyle.body2 + .copyWith(color: context.colorScheme.textDisabled)), + TextSpan( + text: matchResult!.winType + .getString(context, matchResult!.difference), + ), + ])); + } } } diff --git a/khelo/lib/domain/formatter/date_formatter.dart b/khelo/lib/domain/formatter/date_formatter.dart index 478c3ff2..09019756 100644 --- a/khelo/lib/domain/formatter/date_formatter.dart +++ b/khelo/lib/domain/formatter/date_formatter.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:intl/intl.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:style/extensions/date_extensions.dart'; enum DateFormatType { dateAndTime, @@ -42,11 +43,30 @@ extension DateFormatter on DateTime { } } + String relativeTime(BuildContext context) { + final today = DateTime.now(); + final tomorrow = today.add(const Duration(days: 1)); + + String day; + if (isSameDay(today)) { + day = 'Today'; + } else if (isSameDay(tomorrow)) { + day = 'Tomorrow'; + } else if (year == today.year) { + day = DateFormat('d MMM').format(this); + } else { + day = DateFormat('d MMM yyyy').format(this); + } + + return day; + } + static String formatDateRange( BuildContext context, { required DateTime startDate, - required DateTime endDate, + DateTime? endDate, required DateFormatType formatType, }) => - "${startDate.format(context, formatType)} - ${endDate.format(context, formatType)}"; + endDate?.format(context, formatType) ?? + "${startDate.format(context, formatType)} - ${endDate!.format(context, formatType)}"; } diff --git a/khelo/lib/ui/flow/home/home_screen.dart b/khelo/lib/ui/flow/home/home_screen.dart index 1d5facc9..26fa28a6 100644 --- a/khelo/lib/ui/flow/home/home_screen.dart +++ b/khelo/lib/ui/flow/home/home_screen.dart @@ -193,7 +193,7 @@ class _HomeScreenState extends ConsumerState { Visibility( visible: isViewAllShow, child: Text( - context.l10n.home_screen_view_all_btn, + context.l10n.common_view_all, style: AppTextStyle.button.copyWith( color: context.colorScheme.primary, ), diff --git a/khelo/lib/ui/flow/tournament/detail/tabs/tournament_detail_overview_tab.dart b/khelo/lib/ui/flow/tournament/detail/tabs/tournament_detail_overview_tab.dart new file mode 100644 index 00000000..0b9aa8c5 --- /dev/null +++ b/khelo/lib/ui/flow/tournament/detail/tabs/tournament_detail_overview_tab.dart @@ -0,0 +1,436 @@ +import 'package:data/api/match/match_model.dart'; +import 'package:data/api/team/team_model.dart'; +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:khelo/components/won_by_message_text.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/extensions/enum_extensions.dart'; +import 'package:khelo/domain/extensions/string_extensions.dart'; +import 'package:khelo/domain/formatter/date_formatter.dart'; +import 'package:khelo/ui/app_route.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; + +import '../../../../../components/image_avatar.dart'; + +class TournamentDetailOverviewTab extends ConsumerStatefulWidget { + final TournamentModel tournament; + + const TournamentDetailOverviewTab({ + super.key, + required this.tournament, + }); + + @override + ConsumerState createState() => + _TournamentDetailOverviewTabState(); +} + +class _TournamentDetailOverviewTabState + extends ConsumerState { + @override + Widget build(BuildContext context) { + return Container( + color: context.colorScheme.containerLow, + child: ListView( + physics: NeverScrollableScrollPhysics(), + padding: context.mediaQueryPadding.copyWith(top: 0) + + EdgeInsets.symmetric(horizontal: 16), + children: [ + _featuredMatchesView(context, widget.tournament.matches), + _keyStatsView(context, widget.tournament.keyStats), + _teamsSquadsView(context, widget.tournament.teams), + _infoView(context, widget.tournament), + ], + ), + ); + } + + Widget _featuredMatchesView(BuildContext context, List matches) { + if (matches.isEmpty) return SizedBox(); + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Column( + children: [ + _header( + context, + title: + context.l10n.tournament_detail_overview_featured_matches_title, + showViewAll: false, + onViewAll: () {}, + ), + const SizedBox(height: 8), + ...matches.map((match) => _matchCellView(context, match, true)), + ], + ), + ); + } + + Widget _matchCellView( + BuildContext context, MatchModel match, bool isFinalMatch) { + return Container( + padding: isFinalMatch + ? EdgeInsets.only(right: 16) + : EdgeInsets.symmetric(horizontal: 16, vertical: 24), + margin: EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + if (isFinalMatch) ...[ + RotatedBox( + quarterTurns: 7, + child: _finalTag(), + ), + ], + _buildTeamInfo(team: match.teams.first.team), + const Spacer(), + Column( + children: [ + if (match.matchResult != null) ...[ + WonByMessageText( + isTournament: true, + matchResult: match.matchResult, + ), + ] else ...[ + Text( + match.start_at?.format(context, DateFormatType.time) ?? + DateTime.now().format(context, DateFormatType.time), + style: AppTextStyle.caption + .copyWith(color: context.colorScheme.textDisabled), + ), + Text( + match.start_at?.relativeTime(context) ?? + DateTime.now().relativeTime(context), + style: AppTextStyle.subtitle2 + .copyWith(color: context.colorScheme.textPrimary), + ), + ], + ], + ), + const Spacer(), + _buildTeamInfo(team: match.teams.last.team, isSecond: true), + ], + ), + ); + } + + Widget _finalTag() { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: context.colorScheme.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + ), + child: Text( + "Semi Final 1", + style: TextStyle(fontFamily: AppTextStyle.poppinsFontFamily), + ), + ); + } + + Widget _buildTeamInfo({ + required TeamModel team, + bool isSecond = false, + }) { + final initials = team.name_initial ?? team.name.initials(limit: 2); + return Row( + children: [ + if (isSecond) ...[ + Text( + initials, + style: AppTextStyle.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + const SizedBox(width: 16), + ImageAvatar( + initial: initials, + imageUrl: team.profile_img_url, + size: 40, + ), + ] else ...[ + ImageAvatar( + initial: initials, + imageUrl: team.profile_img_url, + size: 40, + ), + const SizedBox(width: 16), + Text( + initials, + style: AppTextStyle.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ], + ); + } + + Widget _keyStatsView(BuildContext context, List keyStats) { + if (keyStats.isEmpty) return SizedBox(); + + return Padding( + padding: const EdgeInsets.only(top: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _header( + context, + showViewAll: keyStats.length > 4, + title: context.l10n.tournament_detail_overview_key_stats_title, + onViewAll: () {}, + ), + const SizedBox(height: 8), + GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: keyStats.take(4).length, + padding: EdgeInsets.zero, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 115, + ), + itemBuilder: (context, index) => + _keyStatsCellView(context, keyStats[index]), + ) + ], + ), + ); + } + + Widget _keyStatsCellView(BuildContext context, PlayerKeyStat keyStat) { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context + .l10n.tournament_detail_overview_key_stats_most_runs_title, + style: AppTextStyle.body2 + .copyWith(color: context.colorScheme.positive), + ), + Text( + keyStat.runs.toString(), + style: AppTextStyle.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + ) + ], + ), + Divider(color: context.colorScheme.outline), + Row( + children: [ + ImageAvatar( + initial: keyStat.player.nameInitial, + imageUrl: keyStat.player.profile_img_url, + size: 40, + ), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + keyStat.player.name ?? '', + style: AppTextStyle.subtitle2.copyWith( + color: context.colorScheme.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + keyStat.teamName, + style: AppTextStyle.caption.copyWith( + color: context.colorScheme.textDisabled, + ), + ) + ], + ), + ) + ], + ), + ], + ), + ); + } + + Widget _teamsSquadsView(BuildContext context, List teams) { + if (teams.isEmpty) return SizedBox(); + + return Padding( + padding: const EdgeInsets.only(top: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _header( + context, + showViewAll: teams.length >= 3, + title: context.l10n.tournament_detail_overview_teams_squads_title, + onViewAll: () {}, + ), + const SizedBox(height: 8), + SizedBox( + height: 130, + child: ListView.separated( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => _teamCellView( + context, + teams[index], + ), + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemCount: teams.length, + ), + ) + ], + ), + ); + } + + Widget _teamCellView(BuildContext context, TeamModel team) { + return OnTapScale( + onTap: () => AppRoute.teamDetail(teamId: team.id).push(context), + child: Container( + width: 120, + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + ImageAvatar( + initial: team.name_initial ?? team.name.initials(limit: 1), + imageUrl: team.profile_img_url, + size: 40, + ), + const SizedBox(height: 16), + Flexible( + child: Text( + team.name, + style: AppTextStyle.subtitle2.copyWith( + color: context.colorScheme.textPrimary, + ), + textAlign: TextAlign.center, + ), + ) + ], + ), + ), + ); + } + + Widget _infoView(BuildContext context, TournamentModel tournament) { + return Padding( + padding: const EdgeInsets.only(top: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _header(context, + title: context.l10n.tournament_detail_overview_info_title), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + _infoCellView( + context, + context.l10n.tournament_detail_overview_info_title, + tournament.name, + ), + Divider(color: context.colorScheme.outline, height: 1), + _infoCellView( + context, + context.l10n.tournament_detail_overview_duration_title, + DateFormatter.formatDateRange( + context, + startDate: tournament.start_date, + endDate: tournament.end_date, + formatType: DateFormatType.dayMonth, + ), + ), + Divider(color: context.colorScheme.outline, height: 1), + _infoCellView( + context, + context.l10n.tournament_detail_overview_type_title, + tournament.type.getString(context), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _infoCellView(BuildContext context, String title, String value) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Text( + title, + style: AppTextStyle.subtitle3 + .copyWith(color: context.colorScheme.textSecondary), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: AppTextStyle.subtitle2 + .copyWith(color: context.colorScheme.textPrimary), + textAlign: TextAlign.end, + ), + ), + ], + ), + ); + } + + Widget _header( + BuildContext context, { + required String title, + bool showViewAll = false, + Function()? onViewAll, + }) { + return Row( + children: [ + Text( + title, + style: AppTextStyle.header4 + .copyWith(color: context.colorScheme.textPrimary), + ), + const Spacer(), + Opacity( + opacity: showViewAll ? 1 : 0, + child: TextButton( + onPressed: onViewAll, + child: Text( + context.l10n.common_view_all, + style: AppTextStyle.subtitle3 + .copyWith(color: context.colorScheme.primary), + ), + ), + ) + ], + ); + } +} diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart index 1d999d02..5dca3c19 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart @@ -8,6 +8,7 @@ import 'package:khelo/components/image_avatar.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/formatter/date_formatter.dart'; import 'package:khelo/ui/flow/tournament/components/sliver_header_delegate.dart'; +import 'package:khelo/ui/flow/tournament/detail/tabs/tournament_detail_overview_tab.dart'; import 'package:khelo/ui/flow/tournament/detail/tournament_detail_view_model.dart'; import 'package:style/button/more_option_button.dart'; import 'package:style/button/tab_button.dart'; @@ -101,7 +102,7 @@ class _TournamentDetailScreenState ), SliverFillRemaining( child: _content(context, state), - ) + ), ], ); } @@ -110,8 +111,10 @@ class _TournamentDetailScreenState return PageView( controller: _controller, onPageChanged: notifier.onTabChange, - children: const [ - //Add Tab view + children: [ + TournamentDetailOverviewTab( + tournament: state.tournament!, + ), ], ); } diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart index d1d53714..e26eedb9 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart @@ -51,6 +51,7 @@ class TournamentDetailStateViewNotifier state = state.copyWith(selectedTab: tab); } } + @override void dispose() { _tournamentSubscription?.cancel(); diff --git a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart index e8b6ed53..b9adfada 100644 --- a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart +++ b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart @@ -203,14 +203,12 @@ class _TournamentListScreenState extends ConsumerState TournamentModel tournament, ) { return Text.rich(TextSpan( - text: (tournament.end_date != null) - ? DateFormatter.formatDateRange( - context, - startDate: tournament.start_date, - endDate: tournament.end_date!, - formatType: DateFormatType.dayMonth, - ) - : tournament.start_date.format(context, DateFormatType.dayMonth), + text: DateFormatter.formatDateRange( + context, + startDate: tournament.start_date, + endDate: tournament.end_date!, + formatType: DateFormatType.dayMonth, + ), style: AppTextStyle.caption .copyWith(color: context.colorScheme.textDisabled), children: [ diff --git a/style/lib/extensions/date_extensions.dart b/style/lib/extensions/date_extensions.dart index f6f75f26..26334e22 100644 --- a/style/lib/extensions/date_extensions.dart +++ b/style/lib/extensions/date_extensions.dart @@ -1,5 +1,9 @@ +import 'package:flutter/material.dart'; + extension DateTimeExtensions on DateTime { DateTime get startOfDay => DateTime(year, month, day); + bool isSameDay(DateTime? date) => DateUtils.isSameDay(this, date); + DateTime get startOfMonth => DateTime(year, month, 1); }