diff --git a/data/lib/api/tournament/tournament_model.dart b/data/lib/api/tournament/tournament_model.dart new file mode 100644 index 00000000..07b9c4c7 --- /dev/null +++ b/data/lib/api/tournament/tournament_model.dart @@ -0,0 +1,83 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../converter/timestamp_json_converter.dart'; +import '../match/match_model.dart'; +import '../team/team_model.dart'; + +part 'tournament_model.freezed.dart'; + +part 'tournament_model.g.dart'; + +@freezed +class TournamentModel with _$TournamentModel { + @JsonSerializable(anyMap: true, explicitToJson: true) + const factory TournamentModel({ + required String id, + required String name, + String? profile_img_url, + String? banner_img_url, + required TournamentType type, + @Default([]) List members, + required String created_by, + @TimeStampJsonConverter() DateTime? created_at, + @TimeStampJsonConverter() required DateTime start_date, + @TimeStampJsonConverter() DateTime? end_date, + @Default([]) List team_ids, + @Default([]) List match_ids, + @JsonKey(includeFromJson: false, includeToJson: false) + @Default([]) + List teams, + @JsonKey(includeFromJson: false, includeToJson: false) + @Default([]) + List matches, + }) = _TournamentModel; + + factory TournamentModel.fromJson(Map json) => + _$TournamentModelFromJson(json); + + factory TournamentModel.fromFireStore( + DocumentSnapshot> snapshot, + SnapshotOptions? options, + ) => + TournamentModel.fromJson(snapshot.data()!); +} + +@freezed +class TournamentMember with _$TournamentMember { + const factory TournamentMember({ + required String id, + @Default(TournamentMemberRole.admin) TournamentMemberRole role, + }) = _TournamentMember; + + factory TournamentMember.fromJson(Map json) => + _$TournamentMemberFromJson(json); +} + +@JsonEnum(valueField: "value") +enum TournamentType { + knockOut(1), + miniRobin(2), + boxLeague(3), + doubleOut(4), + superOver(5), + bestOf(6), + gully(7), + mixed(8), + other(9); + + final int value; + + const TournamentType(this.value); +} + +enum TournamentMemberRole { + organizer, + admin; + + bool get isOrganizer => this == TournamentMemberRole.organizer; + + bool get isAdmin => this == TournamentMemberRole.admin; +} diff --git a/data/lib/api/tournament/tournament_model.freezed.dart b/data/lib/api/tournament/tournament_model.freezed.dart new file mode 100644 index 00000000..9eba15b3 --- /dev/null +++ b/data/lib/api/tournament/tournament_model.freezed.dart @@ -0,0 +1,682 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'tournament_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +TournamentModel _$TournamentModelFromJson(Map json) { + return _TournamentModel.fromJson(json); +} + +/// @nodoc +mixin _$TournamentModel { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String? get profile_img_url => throw _privateConstructorUsedError; + String? get banner_img_url => throw _privateConstructorUsedError; + TournamentType get type => throw _privateConstructorUsedError; + List get members => throw _privateConstructorUsedError; + String get created_by => throw _privateConstructorUsedError; + @TimeStampJsonConverter() + DateTime? get created_at => throw _privateConstructorUsedError; + @TimeStampJsonConverter() + DateTime get start_date => throw _privateConstructorUsedError; + @TimeStampJsonConverter() + DateTime? get end_date => throw _privateConstructorUsedError; + List get team_ids => throw _privateConstructorUsedError; + List get match_ids => throw _privateConstructorUsedError; + @JsonKey(includeFromJson: false, includeToJson: false) + List get teams => throw _privateConstructorUsedError; + @JsonKey(includeFromJson: false, includeToJson: false) + List get matches => throw _privateConstructorUsedError; + + /// Serializes this TournamentModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TournamentModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TournamentModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TournamentModelCopyWith<$Res> { + factory $TournamentModelCopyWith( + TournamentModel value, $Res Function(TournamentModel) then) = + _$TournamentModelCopyWithImpl<$Res, TournamentModel>; + @useResult + $Res call( + {String id, + String name, + String? profile_img_url, + String? banner_img_url, + TournamentType type, + List members, + String created_by, + @TimeStampJsonConverter() DateTime? created_at, + @TimeStampJsonConverter() DateTime start_date, + @TimeStampJsonConverter() DateTime? end_date, + List team_ids, + List match_ids, + @JsonKey(includeFromJson: false, includeToJson: false) + List teams, + @JsonKey(includeFromJson: false, includeToJson: false) + List matches}); +} + +/// @nodoc +class _$TournamentModelCopyWithImpl<$Res, $Val extends TournamentModel> + implements $TournamentModelCopyWith<$Res> { + _$TournamentModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TournamentModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? profile_img_url = freezed, + Object? banner_img_url = freezed, + Object? type = null, + Object? members = null, + Object? created_by = null, + Object? created_at = freezed, + Object? start_date = null, + Object? end_date = freezed, + Object? team_ids = null, + Object? match_ids = null, + Object? teams = null, + Object? matches = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + profile_img_url: freezed == profile_img_url + ? _value.profile_img_url + : profile_img_url // ignore: cast_nullable_to_non_nullable + as String?, + banner_img_url: freezed == banner_img_url + ? _value.banner_img_url + : banner_img_url // ignore: cast_nullable_to_non_nullable + as String?, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as TournamentType, + members: null == members + ? _value.members + : members // ignore: cast_nullable_to_non_nullable + as List, + created_by: null == created_by + ? _value.created_by + : created_by // ignore: cast_nullable_to_non_nullable + as String, + created_at: freezed == created_at + ? _value.created_at + : created_at // ignore: cast_nullable_to_non_nullable + as DateTime?, + start_date: null == start_date + ? _value.start_date + : start_date // ignore: cast_nullable_to_non_nullable + as DateTime, + end_date: freezed == end_date + ? _value.end_date + : end_date // ignore: cast_nullable_to_non_nullable + as DateTime?, + team_ids: null == team_ids + ? _value.team_ids + : team_ids // ignore: cast_nullable_to_non_nullable + as List, + match_ids: null == match_ids + ? _value.match_ids + : match_ids // ignore: cast_nullable_to_non_nullable + as List, + teams: null == teams + ? _value.teams + : teams // ignore: cast_nullable_to_non_nullable + as List, + matches: null == matches + ? _value.matches + : matches // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TournamentModelImplCopyWith<$Res> + implements $TournamentModelCopyWith<$Res> { + factory _$$TournamentModelImplCopyWith(_$TournamentModelImpl value, + $Res Function(_$TournamentModelImpl) then) = + __$$TournamentModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String name, + String? profile_img_url, + String? banner_img_url, + TournamentType type, + List members, + String created_by, + @TimeStampJsonConverter() DateTime? created_at, + @TimeStampJsonConverter() DateTime start_date, + @TimeStampJsonConverter() DateTime? end_date, + List team_ids, + List match_ids, + @JsonKey(includeFromJson: false, includeToJson: false) + List teams, + @JsonKey(includeFromJson: false, includeToJson: false) + List matches}); +} + +/// @nodoc +class __$$TournamentModelImplCopyWithImpl<$Res> + extends _$TournamentModelCopyWithImpl<$Res, _$TournamentModelImpl> + implements _$$TournamentModelImplCopyWith<$Res> { + __$$TournamentModelImplCopyWithImpl( + _$TournamentModelImpl _value, $Res Function(_$TournamentModelImpl) _then) + : super(_value, _then); + + /// Create a copy of TournamentModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? profile_img_url = freezed, + Object? banner_img_url = freezed, + Object? type = null, + Object? members = null, + Object? created_by = null, + Object? created_at = freezed, + Object? start_date = null, + Object? end_date = freezed, + Object? team_ids = null, + Object? match_ids = null, + Object? teams = null, + Object? matches = null, + }) { + return _then(_$TournamentModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + profile_img_url: freezed == profile_img_url + ? _value.profile_img_url + : profile_img_url // ignore: cast_nullable_to_non_nullable + as String?, + banner_img_url: freezed == banner_img_url + ? _value.banner_img_url + : banner_img_url // ignore: cast_nullable_to_non_nullable + as String?, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as TournamentType, + members: null == members + ? _value._members + : members // ignore: cast_nullable_to_non_nullable + as List, + created_by: null == created_by + ? _value.created_by + : created_by // ignore: cast_nullable_to_non_nullable + as String, + created_at: freezed == created_at + ? _value.created_at + : created_at // ignore: cast_nullable_to_non_nullable + as DateTime?, + start_date: null == start_date + ? _value.start_date + : start_date // ignore: cast_nullable_to_non_nullable + as DateTime, + end_date: freezed == end_date + ? _value.end_date + : end_date // ignore: cast_nullable_to_non_nullable + as DateTime?, + team_ids: null == team_ids + ? _value._team_ids + : team_ids // ignore: cast_nullable_to_non_nullable + as List, + match_ids: null == match_ids + ? _value._match_ids + : match_ids // ignore: cast_nullable_to_non_nullable + as List, + teams: null == teams + ? _value._teams + : teams // ignore: cast_nullable_to_non_nullable + as List, + matches: null == matches + ? _value._matches + : matches // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +@JsonSerializable(anyMap: true, explicitToJson: true) +class _$TournamentModelImpl implements _TournamentModel { + const _$TournamentModelImpl( + {required this.id, + required this.name, + this.profile_img_url, + this.banner_img_url, + required this.type, + final List members = const [], + required this.created_by, + @TimeStampJsonConverter() this.created_at, + @TimeStampJsonConverter() required this.start_date, + @TimeStampJsonConverter() this.end_date, + final List team_ids = const [], + final List match_ids = const [], + @JsonKey(includeFromJson: false, includeToJson: false) + final List teams = const [], + @JsonKey(includeFromJson: false, includeToJson: false) + final List matches = const []}) + : _members = members, + _team_ids = team_ids, + _match_ids = match_ids, + _teams = teams, + _matches = matches; + + factory _$TournamentModelImpl.fromJson(Map json) => + _$$TournamentModelImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String? profile_img_url; + @override + final String? banner_img_url; + @override + final TournamentType type; + final List _members; + @override + @JsonKey() + List get members { + if (_members is EqualUnmodifiableListView) return _members; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_members); + } + + @override + final String created_by; + @override + @TimeStampJsonConverter() + final DateTime? created_at; + @override + @TimeStampJsonConverter() + final DateTime start_date; + @override + @TimeStampJsonConverter() + final DateTime? end_date; + final List _team_ids; + @override + @JsonKey() + List get team_ids { + if (_team_ids is EqualUnmodifiableListView) return _team_ids; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_team_ids); + } + + final List _match_ids; + @override + @JsonKey() + List get match_ids { + if (_match_ids is EqualUnmodifiableListView) return _match_ids; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_match_ids); + } + + final List _teams; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + List get teams { + if (_teams is EqualUnmodifiableListView) return _teams; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_teams); + } + + final List _matches; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + List get matches { + if (_matches is EqualUnmodifiableListView) return _matches; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_matches); + } + + @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)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TournamentModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.profile_img_url, profile_img_url) || + other.profile_img_url == profile_img_url) && + (identical(other.banner_img_url, banner_img_url) || + other.banner_img_url == banner_img_url) && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other._members, _members) && + (identical(other.created_by, created_by) || + other.created_by == created_by) && + (identical(other.created_at, created_at) || + other.created_at == created_at) && + (identical(other.start_date, start_date) || + other.start_date == start_date) && + (identical(other.end_date, end_date) || + other.end_date == end_date) && + const DeepCollectionEquality().equals(other._team_ids, _team_ids) && + const DeepCollectionEquality() + .equals(other._match_ids, _match_ids) && + const DeepCollectionEquality().equals(other._teams, _teams) && + const DeepCollectionEquality().equals(other._matches, _matches)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + profile_img_url, + banner_img_url, + type, + const DeepCollectionEquality().hash(_members), + created_by, + created_at, + start_date, + end_date, + const DeepCollectionEquality().hash(_team_ids), + const DeepCollectionEquality().hash(_match_ids), + const DeepCollectionEquality().hash(_teams), + const DeepCollectionEquality().hash(_matches)); + + /// Create a copy of TournamentModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TournamentModelImplCopyWith<_$TournamentModelImpl> get copyWith => + __$$TournamentModelImplCopyWithImpl<_$TournamentModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$TournamentModelImplToJson( + this, + ); + } +} + +abstract class _TournamentModel implements TournamentModel { + const factory _TournamentModel( + {required final String id, + required final String name, + final String? profile_img_url, + final String? banner_img_url, + required final TournamentType type, + final List members, + required final String created_by, + @TimeStampJsonConverter() final DateTime? created_at, + @TimeStampJsonConverter() required final DateTime start_date, + @TimeStampJsonConverter() final DateTime? end_date, + final List team_ids, + final List match_ids, + @JsonKey(includeFromJson: false, includeToJson: false) + final List teams, + @JsonKey(includeFromJson: false, includeToJson: false) + final List matches}) = _$TournamentModelImpl; + + factory _TournamentModel.fromJson(Map json) = + _$TournamentModelImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String? get profile_img_url; + @override + String? get banner_img_url; + @override + TournamentType get type; + @override + List get members; + @override + String get created_by; + @override + @TimeStampJsonConverter() + DateTime? get created_at; + @override + @TimeStampJsonConverter() + DateTime get start_date; + @override + @TimeStampJsonConverter() + DateTime? get end_date; + @override + List get team_ids; + @override + List get match_ids; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + List get teams; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + List get matches; + + /// Create a copy of TournamentModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TournamentModelImplCopyWith<_$TournamentModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +TournamentMember _$TournamentMemberFromJson(Map json) { + return _TournamentMember.fromJson(json); +} + +/// @nodoc +mixin _$TournamentMember { + String get id => throw _privateConstructorUsedError; + TournamentMemberRole get role => throw _privateConstructorUsedError; + + /// Serializes this TournamentMember to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TournamentMember + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TournamentMemberCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TournamentMemberCopyWith<$Res> { + factory $TournamentMemberCopyWith( + TournamentMember value, $Res Function(TournamentMember) then) = + _$TournamentMemberCopyWithImpl<$Res, TournamentMember>; + @useResult + $Res call({String id, TournamentMemberRole role}); +} + +/// @nodoc +class _$TournamentMemberCopyWithImpl<$Res, $Val extends TournamentMember> + implements $TournamentMemberCopyWith<$Res> { + _$TournamentMemberCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TournamentMember + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? role = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as TournamentMemberRole, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TournamentMemberImplCopyWith<$Res> + implements $TournamentMemberCopyWith<$Res> { + factory _$$TournamentMemberImplCopyWith(_$TournamentMemberImpl value, + $Res Function(_$TournamentMemberImpl) then) = + __$$TournamentMemberImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, TournamentMemberRole role}); +} + +/// @nodoc +class __$$TournamentMemberImplCopyWithImpl<$Res> + extends _$TournamentMemberCopyWithImpl<$Res, _$TournamentMemberImpl> + implements _$$TournamentMemberImplCopyWith<$Res> { + __$$TournamentMemberImplCopyWithImpl(_$TournamentMemberImpl _value, + $Res Function(_$TournamentMemberImpl) _then) + : super(_value, _then); + + /// Create a copy of TournamentMember + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? role = null, + }) { + return _then(_$TournamentMemberImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as TournamentMemberRole, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TournamentMemberImpl implements _TournamentMember { + const _$TournamentMemberImpl( + {required this.id, this.role = TournamentMemberRole.admin}); + + factory _$TournamentMemberImpl.fromJson(Map json) => + _$$TournamentMemberImplFromJson(json); + + @override + final String id; + @override + @JsonKey() + final TournamentMemberRole role; + + @override + String toString() { + return 'TournamentMember(id: $id, role: $role)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TournamentMemberImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.role, role) || other.role == role)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, role); + + /// Create a copy of TournamentMember + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TournamentMemberImplCopyWith<_$TournamentMemberImpl> get copyWith => + __$$TournamentMemberImplCopyWithImpl<_$TournamentMemberImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$TournamentMemberImplToJson( + this, + ); + } +} + +abstract class _TournamentMember implements TournamentMember { + const factory _TournamentMember( + {required final String id, + final TournamentMemberRole role}) = _$TournamentMemberImpl; + + factory _TournamentMember.fromJson(Map json) = + _$TournamentMemberImpl.fromJson; + + @override + String get id; + @override + TournamentMemberRole get role; + + /// Create a copy of TournamentMember + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TournamentMemberImplCopyWith<_$TournamentMemberImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/data/lib/api/tournament/tournament_model.g.dart b/data/lib/api/tournament/tournament_model.g.dart new file mode 100644 index 00000000..475a8cd0 --- /dev/null +++ b/data/lib/api/tournament/tournament_model.g.dart @@ -0,0 +1,99 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tournament_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TournamentModelImpl _$$TournamentModelImplFromJson(Map json) => + _$TournamentModelImpl( + id: json['id'] as String, + name: json['name'] as String, + profile_img_url: json['profile_img_url'] as String?, + banner_img_url: json['banner_img_url'] as String?, + type: $enumDecode(_$TournamentTypeEnumMap, json['type']), + members: (json['members'] as List?) + ?.map((e) => TournamentMember.fromJson( + Map.from(e as Map))) + .toList() ?? + const [], + created_by: json['created_by'] as String, + created_at: _$JsonConverterFromJson( + json['created_at'], const TimeStampJsonConverter().fromJson), + start_date: + const TimeStampJsonConverter().fromJson(json['start_date'] as Object), + end_date: _$JsonConverterFromJson( + json['end_date'], const TimeStampJsonConverter().fromJson), + team_ids: (json['team_ids'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + match_ids: (json['match_ids'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); + +Map _$$TournamentModelImplToJson( + _$TournamentModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'profile_img_url': instance.profile_img_url, + 'banner_img_url': instance.banner_img_url, + 'type': _$TournamentTypeEnumMap[instance.type]!, + 'members': instance.members.map((e) => e.toJson()).toList(), + 'created_by': instance.created_by, + 'created_at': _$JsonConverterToJson( + instance.created_at, const TimeStampJsonConverter().toJson), + 'start_date': const TimeStampJsonConverter().toJson(instance.start_date), + 'end_date': _$JsonConverterToJson( + instance.end_date, const TimeStampJsonConverter().toJson), + 'team_ids': instance.team_ids, + 'match_ids': instance.match_ids, + }; + +const _$TournamentTypeEnumMap = { + TournamentType.knockOut: 1, + TournamentType.miniRobin: 2, + TournamentType.boxLeague: 3, + TournamentType.doubleOut: 4, + TournamentType.superOver: 5, + TournamentType.bestOf: 6, + TournamentType.gully: 7, + TournamentType.mixed: 8, + TournamentType.other: 9, +}; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + +_$TournamentMemberImpl _$$TournamentMemberImplFromJson( + Map json) => + _$TournamentMemberImpl( + id: json['id'] as String, + role: $enumDecodeNullable(_$TournamentMemberRoleEnumMap, json['role']) ?? + TournamentMemberRole.admin, + ); + +Map _$$TournamentMemberImplToJson( + _$TournamentMemberImpl instance) => + { + 'id': instance.id, + 'role': _$TournamentMemberRoleEnumMap[instance.role]!, + }; + +const _$TournamentMemberRoleEnumMap = { + TournamentMemberRole.organizer: 'organizer', + TournamentMemberRole.admin: 'admin', +}; diff --git a/data/lib/service/tournament/tournament_service.dart b/data/lib/service/tournament/tournament_service.dart new file mode 100644 index 00000000..093b66ff --- /dev/null +++ b/data/lib/service/tournament/tournament_service.dart @@ -0,0 +1,52 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../api/tournament/tournament_model.dart'; +import '../../errors/app_error.dart'; +import '../../utils/constant/firestore_constant.dart'; + +final tournamentServiceProvider = + Provider((ref) => TournamentService(FirebaseFirestore.instance)); + +class TournamentService { + final FirebaseFirestore _firestore; + + TournamentService(this._firestore); + + CollectionReference get _tournamentCollection => + _firestore.collection(FireStoreConst.tournamentCollection).withConverter( + fromFirestore: TournamentModel.fromFireStore, + toFirestore: (TournamentModel tournament, _) => tournament.toJson(), + ); + + String get generateTournamentId => _tournamentCollection.doc().id; + + Future createTournament(TournamentModel tournament) async { + try { + await _tournamentCollection + .doc(tournament.id) + .set(tournament, SetOptions(merge: true)); + } catch (error, stack) { + throw AppError.fromError(error, stack); + } + } + + Stream> streamCurrentUserRelatedMatches(String userId) { + final currentMember = TournamentMember(id: userId); + + final memberContains = [ + currentMember.copyWith(role: TournamentMemberRole.organizer).toJson(), + currentMember.copyWith(role: TournamentMemberRole.admin).toJson(), + ]; + final filter = Filter.or( + Filter(FireStoreConst.createdBy, isEqualTo: userId), + Filter(FireStoreConst.members, arrayContainsAny: memberContains), + ); + + return _tournamentCollection + .where(filter) + .snapshots() + .map((event) => event.docs.map((e) => e.data()).toList()) + .handleError((error, stack) => throw AppError.fromError(error, stack)); + } +} diff --git a/data/lib/utils/constant/firebase_storage_constant.dart b/data/lib/utils/constant/firebase_storage_constant.dart index 053f8109..2c1cd5be 100644 --- a/data/lib/utils/constant/firebase_storage_constant.dart +++ b/data/lib/utils/constant/firebase_storage_constant.dart @@ -17,4 +17,16 @@ class StorageConst { required String imageName, }) => "$rootDirectory/$userId/support_attachment_images/$imageName"; + + static String tournamentProfileUploadPath({ + required String userId, + required String tournamentId, + }) => + "$rootDirectory/$userId/tournament_images/$tournamentId/profile"; + + static String tournamentBannerUploadPath({ + required String userId, + required String tournamentId, + }) => + "$rootDirectory/$userId/tournament_images/$tournamentId/banner"; } diff --git a/data/lib/utils/constant/firestore_constant.dart b/data/lib/utils/constant/firestore_constant.dart index 5604467d..76f9f142 100644 --- a/data/lib/utils/constant/firestore_constant.dart +++ b/data/lib/utils/constant/firestore_constant.dart @@ -7,6 +7,7 @@ class FireStoreConst { static const String usersCollection = "users"; static const String userSessionCollection = "user_sessions"; static const String supportCollection = "contact_support"; + static const String tournamentCollection = "tournaments"; // matches field const static const String id = "id"; @@ -50,7 +51,10 @@ class FireStoreConst { static const String notifications = "notifications"; static const String phone = "phone"; static const String name = "name"; -} + + // tournament field const + static const String members = "members"; + } class DataConfig { static late DataConfig _instance; @@ -66,4 +70,4 @@ class DataConfig { DataConfig({ required this.apiBaseUrl, }); -} \ No newline at end of file +} diff --git a/khelo/assets/images/ic_arrow_forward.svg b/khelo/assets/images/ic_arrow_forward.svg new file mode 100644 index 00000000..d21845ad --- /dev/null +++ b/khelo/assets/images/ic_arrow_forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/khelo/assets/images/ic_tournaments.svg b/khelo/assets/images/ic_tournaments.svg new file mode 100644 index 00000000..df8bc04e --- /dev/null +++ b/khelo/assets/images/ic_tournaments.svg @@ -0,0 +1,4 @@ + + + + diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index 0f08f1aa..40b5e33b 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -18,6 +18,7 @@ "common_retry_title": "Retry", "common_submit_title": "Submit", "common_not_now": "Not now", + "common_create": "Create", "common_not_specified_title": "Not Specified", "common_runs_title": "{count, plural, =0{{count} runs} =1{{count} run} other{{count} runs}}", "@common_runs_title": { @@ -138,8 +139,54 @@ "@_MY_CRICKET": { }, - "my_game_teams_tab_title": "Teams", + "my_cricket_teams_tab_title": "Teams", "my_cricket_screen_title": "My Cricket", + "my_cricket_tournament_title": "Tournament", + + "@_ADD_TOURNAMENT": { + }, + "add_tournament_screen_title": "Add Tournament", + "add_tournament_name": "Tournament Name", + "add_tournament_type": "Tournament Type", + "add_tournament_type_placeholder": "Select tournament type", + "add_tournament_start_date": "Start Date", + "add_tournament_end_date": "End Date", + "add_tournament_edit_banner": "Edit banner", + "add_tournament_add_banner_placeholder": "Add banner image", + "add_tournament_date_error": "End date should be greater than start date", + + "tournament_list_empty_list_title": "No tournaments created", + "tournament_list_empty_list_description": "Tap on the “ + ” icon to create a tournament", + "tournament_list_match_title": "{count, plural, =0{No matches} =1{{count} Match} other{{count} Matches}}", + "@tournament_list_match_title": { + "placeholders": { + "count": {} + } + }, + + "@_TOURNAMENT_TYPE":{ + }, + "tournament_type_knock_out": "Knockout", + "tournament_type_mini_robin": "Mini Robin", + "tournament_type_box_league": "Box League", + "tournament_type_double_out": "Double Out", + "tournament_type_super_over": "Super Over", + "tournament_type_best_of": "Best Of", + "tournament_type_gully": "Gully", + "tournament_type_mixed": "Mixed", + "tournament_type_other": "Other", + + "tournament_type_knock_out_description": "Teams face off in a single elimination, with the loser immediately knocked out.", + "tournament_type_mini_robin_description": "A smaller round-robin format where each team plays once against all others.", + "tournament_type_box_league_description": "Teams are divided into groups, with top teams advancing to the knockout stage.", + "tournament_type_double_out_description": "Teams get two chances before being knocked out, with a winners and losers bracket.", + "tournament_type_super_over_description": "A knockout format with a super over to decide tied matches.", + "tournament_type_best_of_description": "Teams play a series of matches, and the first to win the majority is the champion.", + "tournament_type_gully_description": "Casual street-style cricket with short games and flexible rules.", + "tournament_type_mixed_description": "Teams are randomly mixed, creating fun and unpredictable matches", + "tournament_type_other_description": "A custom format for tournaments with unique rules or structures.", + + "add_team_screen_title": "Add Team", "add_team_edit_team_screen_title": "Edit Team", diff --git a/khelo/lib/components/action_bottom_sheet.dart b/khelo/lib/components/action_bottom_sheet.dart index 5a257856..2fad8ee9 100644 --- a/khelo/lib/components/action_bottom_sheet.dart +++ b/khelo/lib/components/action_bottom_sheet.dart @@ -10,6 +10,7 @@ Future showActionBottomSheet({ required List items, bool useRootNavigator = true, bool showDragHandle = true, + double? heightFactor, }) async { HapticFeedback.mediumImpact(); return await showModalBottomSheet( @@ -22,27 +23,31 @@ Future showActionBottomSheet({ useRootNavigator: useRootNavigator, isScrollControlled: true, showDragHandle: showDragHandle, + useSafeArea: true, context: context, - builder: (context) => SingleChildScrollView( - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), + builder: (context) => FractionallySizedBox( + heightFactor: heightFactor, + child: SingleChildScrollView( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + color: context.colorScheme.surface, ), - color: context.colorScheme.surface, - ), - padding: EdgeInsets.only( - bottom: context.mediaQueryPadding.bottom, - ), - child: ColumnBuilder.separated( - separatorBuilder: (index) => Divider( - height: 0, - thickness: 1, - color: context.colorScheme.outline, + padding: EdgeInsets.only( + bottom: context.mediaQueryPadding.bottom, + ), + child: ColumnBuilder.separated( + separatorBuilder: (index) => Divider( + height: 0, + thickness: 1, + color: context.colorScheme.outline, + ), + itemBuilder: (index) => items[index], + itemCount: items.length, + mainAxisSize: MainAxisSize.min, ), - itemBuilder: (index) => items[index], - itemCount: items.length, - mainAxisSize: MainAxisSize.min, ), ), ), @@ -54,6 +59,7 @@ class BottomSheetAction extends StatelessWidget { final String title; final Widget? child; final bool enabled; + final String? subTitle; final void Function()? onTap; const BottomSheetAction({ @@ -62,6 +68,7 @@ class BottomSheetAction extends StatelessWidget { required this.title, this.enabled = true, this.child, + this.subTitle, this.onTap, }); @@ -80,10 +87,24 @@ class BottomSheetAction extends StatelessWidget { child: const SizedBox(width: 20), ), Expanded( - child: Text( - title, - style: AppTextStyle.subtitle2 - .copyWith(color: context.colorScheme.textPrimary), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTextStyle.subtitle2 + .copyWith(color: context.colorScheme.textPrimary), + ), + if (subTitle != null) ...[ + const SizedBox(height: 4), + Text( + subTitle ?? '', + style: AppTextStyle.caption.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + ], + ], ), ), Visibility( diff --git a/khelo/lib/components/profile_image_avatar.dart b/khelo/lib/components/profile_image_avatar.dart index 3dc55605..7cb30b38 100644 --- a/khelo/lib/components/profile_image_avatar.dart +++ b/khelo/lib/components/profile_image_avatar.dart @@ -15,6 +15,7 @@ class ProfileImageAvatar extends StatelessWidget { final String? filePath; final String? placeHolderImage; final bool isLoading; + final Alignment alignment; final Function() onEditButtonTap; const ProfileImageAvatar({ @@ -25,11 +26,13 @@ class ProfileImageAvatar extends StatelessWidget { this.placeHolderImage, required this.isLoading, required this.onEditButtonTap, + this.alignment = Alignment.center, }); @override Widget build(BuildContext context) { - return Center( + return Align( + alignment: alignment, child: SizedBox( height: size, width: size, diff --git a/khelo/lib/domain/extensions/enum_extensions.dart b/khelo/lib/domain/extensions/enum_extensions.dart index 85b28b7c..7db17e44 100644 --- a/khelo/lib/domain/extensions/enum_extensions.dart +++ b/khelo/lib/domain/extensions/enum_extensions.dart @@ -1,5 +1,6 @@ import 'package:data/api/ball_score/ball_score_model.dart'; import 'package:data/api/match/match_model.dart'; +import 'package:data/api/tournament/tournament_model.dart'; import 'package:data/api/user/user_models.dart'; import 'package:flutter/cupertino.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; @@ -265,3 +266,51 @@ extension WinnerByTypeString on WinnerByType { } } } + +extension TournamentTypeString on TournamentType { + String getString(BuildContext context) { + switch (this) { + case TournamentType.knockOut: + return context.l10n.tournament_type_knock_out; + case TournamentType.miniRobin: + return context.l10n.tournament_type_mini_robin; + case TournamentType.boxLeague: + return context.l10n.tournament_type_box_league; + case TournamentType.doubleOut: + return context.l10n.tournament_type_double_out; + case TournamentType.superOver: + return context.l10n.tournament_type_super_over; + case TournamentType.bestOf: + return context.l10n.tournament_type_best_of; + case TournamentType.gully: + return context.l10n.tournament_type_gully; + case TournamentType.mixed: + return context.l10n.tournament_type_mixed; + case TournamentType.other: + return context.l10n.tournament_type_other; + } + } + + String getDescriptionString(BuildContext context) { + switch (this) { + case TournamentType.knockOut: + return context.l10n.tournament_type_knock_out_description; + case TournamentType.miniRobin: + return context.l10n.tournament_type_mini_robin_description; + case TournamentType.boxLeague: + return context.l10n.tournament_type_box_league_description; + case TournamentType.doubleOut: + return context.l10n.tournament_type_double_out_description; + case TournamentType.superOver: + return context.l10n.tournament_type_super_over_description; + case TournamentType.bestOf: + return context.l10n.tournament_type_best_of_description; + case TournamentType.gully: + return context.l10n.tournament_type_gully_description; + case TournamentType.mixed: + return context.l10n.tournament_type_mixed_description; + case TournamentType.other: + return context.l10n.tournament_type_other_description; + } + } +} diff --git a/khelo/lib/domain/formatter/date_formatter.dart b/khelo/lib/domain/formatter/date_formatter.dart index 174cc793..478c3ff2 100644 --- a/khelo/lib/domain/formatter/date_formatter.dart +++ b/khelo/lib/domain/formatter/date_formatter.dart @@ -6,6 +6,8 @@ enum DateFormatType { dateAndTime, date, time, + dayMonth, + monthYear, shortDate, shortDateTime, dayMonthYear @@ -33,6 +35,18 @@ extension DateFormatter on DateTime { .format(this); case DateFormatType.dayMonthYear: return DateFormat.yMMMd().format(this); + case DateFormatType.monthYear: + return DateFormat('MMMM yyyy').format(this); + case DateFormatType.dayMonth: + return DateFormat('dd MMM').format(this); } } + + static String formatDateRange( + BuildContext context, { + required DateTime startDate, + required DateTime endDate, + required DateFormatType formatType, + }) => + "${startDate.format(context, formatType)} - ${endDate.format(context, formatType)}"; } diff --git a/khelo/lib/gen/assets.gen.dart b/khelo/lib/gen/assets.gen.dart index 5c557031..51e19492 100644 --- a/khelo/lib/gen/assets.gen.dart +++ b/khelo/lib/gen/assets.gen.dart @@ -22,6 +22,9 @@ class $AssetsImagesGen { /// File path: assets/images/ic_arrow_down.svg String get icArrowDown => 'assets/images/ic_arrow_down.svg'; + /// File path: assets/images/ic_arrow_forward.svg + String get icArrowForward => 'assets/images/ic_arrow_forward.svg'; + /// File path: assets/images/ic_bat_selected.svg String get icBatSelected => 'assets/images/ic_bat_selected.svg'; @@ -118,6 +121,9 @@ class $AssetsImagesGen { /// File path: assets/images/ic_time.svg String get icTime => 'assets/images/ic_time.svg'; + /// File path: assets/images/ic_tournaments.svg + String get icTournaments => 'assets/images/ic_tournaments.svg'; + /// File path: assets/images/ic_umpire.svg String get icUmpire => 'assets/images/ic_umpire.svg'; @@ -148,6 +154,7 @@ class $AssetsImagesGen { bowler, icAboutUs, icArrowDown, + icArrowForward, icBatSelected, icBin, icCalendar, @@ -180,6 +187,7 @@ class $AssetsImagesGen { icStats, icTermsConditions, icTime, + icTournaments, icUmpire, introCricketDark, introCricketLight, diff --git a/khelo/lib/ui/app_route.dart b/khelo/lib/ui/app_route.dart index 789962a3..4b90d3f6 100644 --- a/khelo/lib/ui/app_route.dart +++ b/khelo/lib/ui/app_route.dart @@ -26,6 +26,7 @@ import 'package:khelo/ui/flow/team/detail/team_detail_screen.dart'; import 'package:khelo/ui/flow/team/scanner/scanner_screen.dart'; import 'package:khelo/ui/flow/team/search_team/search_team_screen.dart'; +import 'package:khelo/ui/flow/tournament/add/add_tournament_screen.dart'; import 'flow/home/view_all/home_view_all_screen.dart'; import 'flow/main/main_screen.dart'; import 'flow/settings/support/contact_support_screen.dart'; @@ -53,6 +54,7 @@ class AppRoute { static const pathSearchHome = "/search-home"; static const pathViewAll = "/view-all"; static const pathContactSelection = "/contact-selection"; + static const pathAddTournament = "/add-tournament"; final String path; final String? name; @@ -165,6 +167,11 @@ class AppRoute { ), ); + static AppRoute addTournament() => AppRoute( + pathAddTournament, + builder: (_) => const AddTournamentScreen(), + ); + static AppRoute matchDetailTab({required String matchId}) => AppRoute( pathMatchDetailTab, builder: (_) => MatchDetailTabScreen(matchId: matchId), @@ -319,6 +326,10 @@ class AppRoute { path: pathAddMatch, builder: (context, state) => state.widget(context), ), + GoRoute( + path: pathAddTournament, + builder: (context, state) => state.widget(context), + ), GoRoute( path: pathMatchDetailTab, builder: (context, state) => state.widget(context), diff --git a/khelo/lib/ui/flow/matches/add_match/add_match_screen.dart b/khelo/lib/ui/flow/matches/add_match/add_match_screen.dart index b8b12928..7889c126 100644 --- a/khelo/lib/ui/flow/matches/add_match/add_match_screen.dart +++ b/khelo/lib/ui/flow/matches/add_match/add_match_screen.dart @@ -27,10 +27,10 @@ import 'package:style/button/bottom_sticky_overlay.dart'; import 'package:style/button/primary_button.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; +import 'package:style/pickers/date_and_time_picker.dart'; import 'package:style/text/app_text_field.dart'; import 'package:style/text/app_text_style.dart'; import 'package:data/api/match/match_model.dart'; -import 'package:style/theme/colors.dart'; import 'package:style/widgets/adaptive_outlined_tile.dart'; import '../../../../components/confirmation_dialog.dart'; @@ -320,7 +320,12 @@ class _AddMatchScreenState extends ConsumerState { headerText: context.l10n.add_match_date_title, placeholder: context.l10n.add_match_date_title, onTap: () { - _selectDate(context, notifier, state); + selectDate( + context, + initialDate: state.matchTime, + onDateSelected: (selectedDate) => + notifier.onDateSelect(selectedDate: selectedDate), + ); }), ), const SizedBox(width: 16), @@ -331,7 +336,12 @@ class _AddMatchScreenState extends ConsumerState { headerText: context.l10n.add_match_time_title, placeholder: context.l10n.add_match_time_title, onTap: () { - _selectTime(context, notifier, state); + selectTime( + context, + initialTime: state.matchTime, + onTimeSelected: (selectedTime) => + notifier.onDateSelect(selectedDate: selectedTime), + ); }), ), ], @@ -341,71 +351,6 @@ class _AddMatchScreenState extends ConsumerState { ); } - Future _selectDate( - BuildContext context, - AddMatchViewNotifier notifier, - AddMatchViewState state, - ) async { - showDatePicker( - context: context, - initialDate: state.matchTime, - firstDate: DateTime(1965), - lastDate: DateTime(2101), - builder: (context, child) { - return Theme( - data: context.brightness == Brightness.dark - ? materialThemeDataDark - : materialThemeDataLight, - child: child!, - ); - }, - ).then((selectedDate) { - if (selectedDate != null) { - DateTime selectedDateTime = DateTime( - selectedDate.year, - selectedDate.month, - selectedDate.day, - state.matchTime.hour, - state.matchTime.minute, - ); - notifier.onDateSelect(selectedDate: selectedDateTime); - } - }); - } - - Future _selectTime( - BuildContext context, - AddMatchViewNotifier notifier, - AddMatchViewState state, - ) async { - showTimePicker( - context: context, - initialTime: TimeOfDay( - hour: state.matchTime.hour, - minute: state.matchTime.minute, - ), - builder: (context, child) { - return Theme( - data: context.brightness == Brightness.dark - ? materialThemeDataDark - : materialThemeDataLight, - child: child!, - ); - }, - ).then((selectedTime) { - if (selectedTime != null) { - DateTime selectedDateTime = DateTime( - state.matchTime.year, - state.matchTime.month, - state.matchTime.day, - selectedTime.hour, - selectedTime.minute, - ); - notifier.onDateSelect(selectedDate: selectedDateTime); - } - }); - } - void _observeActionError(BuildContext context, WidgetRef ref) { ref.listen(addMatchViewStateProvider.select((value) => value.actionError), (previous, next) { diff --git a/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart b/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart index 682d3d21..a3caf408 100644 --- a/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart +++ b/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart @@ -8,6 +8,7 @@ import 'package:khelo/ui/flow/matches/match_list_screen.dart'; import 'package:khelo/ui/flow/my_game/my_game_tab_view_model.dart'; import 'package:khelo/ui/flow/team/team_list_screen.dart'; import 'package:khelo/ui/flow/team/team_list_view_model.dart'; +import 'package:khelo/ui/flow/tournament/tournament_list_screen.dart'; import 'package:style/button/action_button.dart'; import 'package:style/button/tab_button.dart'; import 'package:style/extensions/context_extensions.dart'; @@ -24,6 +25,7 @@ class _MyGameTabScreenState extends ConsumerState final List _tabs = [ const MatchListScreen(), const TeamListScreen(), + const TournamentListScreen(), ]; late PageController _controller; @@ -89,40 +91,76 @@ class _MyGameTabScreenState extends ConsumerState padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ - const SizedBox(width: 16), - TabButton( - context.l10n.common_matches_title, - selected: _selectedTab == 0, - onTap: () { - _controller.jumpToPage(0); - }, + Expanded( + child: SizedBox( + height: 36, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + const SizedBox(width: 16), + TabButton( + context.l10n.common_matches_title, + selected: _selectedTab == 0, + onTap: () { + _controller.jumpToPage(0); + }, + ), + const SizedBox(width: 8), + TabButton( + context.l10n.my_cricket_teams_tab_title, + selected: _selectedTab == 1, + onTap: () { + _controller.jumpToPage(1); + }, + ), + const SizedBox(width: 8), + TabButton( + context.l10n.my_cricket_tournament_title, + selected: _selectedTab == 2, + onTap: () { + _controller.jumpToPage(2); + }, + ), + const SizedBox(width: 16), + ], + ), + ), ), - const SizedBox(width: 8), - TabButton( - context.l10n.my_game_teams_tab_title, - selected: _selectedTab == 1, - onTap: () { - _controller.jumpToPage(1); - }, + Container( + color: context.colorScheme.surface, + child: Row( + children: [ + if (_selectedTab == 1 && + ref.watch(teamListViewStateProvider).teams.isNotEmpty) ...[ + actionButton( + context, + onPressed: () => ref + .read(teamListViewStateProvider.notifier) + .onFilterButtonTap(), + icon: Icon( + CupertinoIcons.slider_horizontal_3, + color: context.colorScheme.textPrimary, + ), + ), + ], + actionButton( + context, + onPressed: () { + final actions = [ + AppRoute.addMatch(), + AppRoute.addTeam(), + AppRoute.addTournament(), + ]; + actions[_selectedTab].push(context); + }, + icon: Icon( + Icons.add, + color: context.colorScheme.textPrimary, + ), + ), + ], + ), ), - const Spacer(), - if (_selectedTab == 1 && - ref.watch(teamListViewStateProvider).teams.isNotEmpty) ...[ - actionButton(context, - onPressed: () => ref - .read(teamListViewStateProvider.notifier) - .onFilterButtonTap(), - icon: Icon( - CupertinoIcons.slider_horizontal_3, - color: context.colorScheme.textPrimary, - )), - ], - actionButton(context, - onPressed: () => _selectedTab == 1 - ? AppRoute.addTeam().push(context) - : AppRoute.addMatch().push(context), - icon: Icon(Icons.add, color: context.colorScheme.textPrimary)), - const SizedBox(width: 8), ], ), ); diff --git a/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart b/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart index a2459dc7..422e4af6 100644 --- a/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart +++ b/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart @@ -18,9 +18,9 @@ import 'package:khelo/ui/flow/settings/edit_profile/edit_profile_view_model.dart import 'package:style/button/action_button.dart'; import 'package:style/button/primary_button.dart'; import 'package:style/extensions/context_extensions.dart'; +import 'package:style/pickers/date_and_time_picker.dart'; import 'package:style/text/app_text_field.dart'; import 'package:style/text/app_text_style.dart'; -import 'package:style/theme/colors.dart'; import 'package:style/widgets/adaptive_outlined_tile.dart'; import '../../../../components/image_picker_sheet.dart'; @@ -173,7 +173,16 @@ class EditProfileScreen extends ConsumerWidget { title: state.dob.format(context, DateFormatType.shortDate), showTrailingIcon: true, placeholder: context.l10n.edit_profile_dob_placeholder, - onTap: () => _selectDate(context, notifier, state), + onTap: () => selectDate( + context, + helpText: context.l10n.edit_profile_select_birth_date_placeholder, + initialDate: state.dob, + onDateSelected: (date) { + if (date != state.dob) { + notifier.onDateSelect(selectedDate: date); + } + }, + ), ), ), const SizedBox(width: 16), @@ -298,31 +307,6 @@ class EditProfileScreen extends ConsumerWidget { : null; } - Future _selectDate( - BuildContext context, - EditProfileViewNotifier notifier, - EditProfileState state, - ) async { - final DateTime? picked = await showDatePicker( - context: context, - helpText: context.l10n.edit_profile_select_birth_date_placeholder, - initialDate: state.dob, - firstDate: DateTime(1920), - lastDate: DateTime.now(), - builder: (context, child) { - return Theme( - data: context.brightness == Brightness.dark - ? materialThemeDataDark - : materialThemeDataLight, - child: child!, - ); - }, - ); - if (picked != null && picked != state.dob) { - notifier.onDateSelect(selectedDate: picked); - } - } - Widget _deleteButton( BuildContext context, { required Function() onDelete, diff --git a/khelo/lib/ui/flow/tournament/add/add_tournament_screen.dart b/khelo/lib/ui/flow/tournament/add/add_tournament_screen.dart new file mode 100644 index 00000000..b0e1d184 --- /dev/null +++ b/khelo/lib/ui/flow/tournament/add/add_tournament_screen.dart @@ -0,0 +1,359 @@ +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:khelo/components/profile_image_avatar.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/extensions/enum_extensions.dart'; +import 'package:khelo/gen/assets.gen.dart'; +import 'package:khelo/ui/flow/tournament/add/add_tournament_view_model.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/button/bottom_sticky_overlay.dart'; +import 'package:style/button/primary_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicator/progress_indicator.dart'; +import 'package:style/pickers/date_and_time_picker.dart'; +import 'package:style/text/app_text_field.dart'; +import 'package:style/text/app_text_style.dart'; +import 'package:style/widgets/adaptive_outlined_tile.dart'; + +import '../../../../components/action_bottom_sheet.dart'; +import '../../../../components/app_page.dart'; +import '../../../../components/error_snackbar.dart'; +import '../../../../components/image_picker_sheet.dart'; +import '../../../../domain/formatter/date_formatter.dart'; + +class AddTournamentScreen extends ConsumerStatefulWidget { + const AddTournamentScreen({super.key}); + + @override + ConsumerState createState() => + _AddTournamentScreenState(); +} + +class _AddTournamentScreenState extends ConsumerState { + late AddTournamentViewNotifier notifier; + + @override + void initState() { + super.initState(); + notifier = ref.read(addTournamentStateProvider.notifier); + } + + void _observeActionError(BuildContext context, WidgetRef ref) { + ref.listen(addTournamentStateProvider.select((value) => value.actionError), + (previous, next) { + if (next != null) { + showErrorSnackBar(context: context, error: next); + } + }); + } + + void _observePop(BuildContext context, WidgetRef ref) { + ref.listen(addTournamentStateProvider.select((value) => value.pop), + (previous, next) { + if (next) { + context.pop(); + } + }); + } + + void _observeDateError(BuildContext context, WidgetRef ref) { + ref.listen( + addTournamentStateProvider.select((value) => value.showDateError), + (previous, next) { + showErrorSnackBar( + context: context, error: context.l10n.add_tournament_date_error); + }); + } + + @override + Widget build(BuildContext context) { + _observeActionError(context, ref); + _observeDateError(context, ref); + _observePop(context, ref); + + final state = ref.watch(addTournamentStateProvider); + + return AppPage( + title: context.l10n.add_tournament_screen_title, + body: Builder( + builder: (context) => Stack( + children: [ + _body(context, state), + _stickyButton(context, state), + ], + ), + ), + ); + } + + Widget _body(BuildContext context, AddTournamentState state) { + return ListView( + padding: context.mediaQueryPadding + BottomStickyOverlay.padding, + children: [ + _bannerView(context, state), + Padding( + padding: const EdgeInsets.all(16).copyWith(top: 16), + child: Column( + children: [ + AppTextField( + controller: state.nameController, + label: context.l10n.add_tournament_name, + onChanged: (_) => notifier.onChange(), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + borderRadius: BorderRadius.circular(12), + borderType: AppTextFieldBorderType.outline, + borderColor: BorderColor( + focusColor: context.colorScheme.outline, + unFocusColor: context.colorScheme.outline, + ), + ), + const SizedBox(height: 16), + AdaptiveOutlinedTile( + placeholder: context.l10n.add_tournament_type_placeholder, + headerText: context.l10n.add_tournament_type, + title: state.selectedType.getString(context), + showTrailingIcon: true, + onTap: () { + _selectTypeSheet( + context, + selectedType: state.selectedType, + onSelected: (type) => notifier.onSelectType(type), + ); + }, + ), + const SizedBox(height: 16), + _dateScheduleView(context, state) + ], + ), + ), + ], + ); + } + + Widget _bannerView(BuildContext context, AddTournamentState state) { + final bool hasBannerImage = + state.bannerPath != null || state.bannerImgUrl != null; + + return Stack( + children: [ + Container( + height: 204, + width: context.mediaQuerySize.width, + margin: EdgeInsets.only(bottom: 45), + decoration: BoxDecoration( + color: context.colorScheme.containerLow, + image: state.bannerPath != null + ? DecorationImage( + image: FileImage(File(state.bannerPath!)), + fit: BoxFit.cover, + ) + : null, + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (state.imageUploading) Center(child: AppProgressIndicator()), + if (!state.imageUploading) + hasBannerImage + ? _buildCachedNetworkOrFileImage(context, state) + : _bannerPlaceholder(context, + onTap: () => _pickImage(isBanner: true)), + if (hasBannerImage && !state.imageUploading) + Positioned( + bottom: 16, + right: 16, + child: _editBannerButton(context), + ), + ], + ), + ), + _profileView(context, state), + ], + ); + } + + Widget _buildCachedNetworkOrFileImage( + BuildContext context, AddTournamentState state) { + if (state.bannerPath != null) { + return Image.file( + File(state.bannerPath!), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ); + } else if (state.bannerImgUrl != null) { + return CachedNetworkImage( + imageUrl: state.bannerImgUrl!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ); + } + return SizedBox.shrink(); // Fallback if neither image is available + } + + Widget _editBannerButton(BuildContext context) { + return TextButton.icon( + onPressed: () => _pickImage(isBanner: true), + icon: SvgPicture.asset( + Assets.images.icCamera, + colorFilter: ColorFilter.mode( + context.colorScheme.surface, + BlendMode.srcATop, + ), + ), + label: Text( + context.l10n.add_tournament_edit_banner, + style: + AppTextStyle.caption.copyWith(color: context.colorScheme.surface), + ), + style: TextButton.styleFrom( + backgroundColor: context.colorScheme.textDisabled, + ), + ); + } + + void _pickImage({bool isBanner = false}) async { + final imagePath = await ImagePickerSheet.show(context, true); + if (imagePath != null) { + notifier.onImageChange(imagePath: imagePath, isBanner: isBanner); + } + } + + Widget _bannerPlaceholder( + BuildContext context, { + required Function() onTap, + }) { + return OnTapScale( + onTap: onTap, + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + SvgPicture.asset( + Assets.images.icCamera, + colorFilter: ColorFilter.mode( + context.colorScheme.textDisabled, + BlendMode.srcATop, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.add_tournament_add_banner_placeholder, + style: AppTextStyle.caption.copyWith( + color: context.colorScheme.textDisabled, + ), + ), + ]), + ); + } + + Widget _stickyButton( + BuildContext context, + AddTournamentState state, + ) { + return BottomStickyOverlay( + child: PrimaryButton( + context.l10n.common_create, + progress: state.loading, + enabled: state.enableButton, + onPressed: notifier.addTournament, + ), + ); + } + + Widget _profileView(BuildContext context, AddTournamentState state) { + return Positioned( + left: 16, + bottom: 0, + child: ProfileImageAvatar( + size: 90, + isLoading: state.imageUploading && state.profilePath == null, + filePath: state.profilePath, + imageUrl: state.profileImgUrl, + placeHolderImage: Assets.images.icTournaments, + alignment: Alignment.centerLeft, + onEditButtonTap: _pickImage, + ), + ); + } + + Widget _dateScheduleView( + BuildContext context, + AddTournamentState state, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: AdaptiveOutlinedTile( + title: state.startDate.format(context, DateFormatType.date), + headerText: context.l10n.add_tournament_start_date, + placeholder: context.l10n.add_tournament_start_date, + onTap: () { + selectDate( + context, + initialDate: state.startDate, + onDateSelected: notifier.onStartDate, + ); + }), + ), + const SizedBox(width: 16), + Expanded( + child: AdaptiveOutlinedTile( + title: state.endDate.format(context, DateFormatType.date), + headerText: context.l10n.add_tournament_end_date, + placeholder: context.l10n.add_tournament_end_date, + onTap: () { + selectDate( + context, + initialDate: state.endDate, + onDateSelected: notifier.onEndDate, + ); + }), + ), + ], + ), + ], + ); + } + + void _selectTypeSheet( + BuildContext context, { + required TournamentType selectedType, + required Function(TournamentType) onSelected, + }) { + showActionBottomSheet( + context: context, + heightFactor: 0.8, + items: TournamentType.values + .map( + (type) => BottomSheetAction( + title: type.getString(context), + subTitle: type.getDescriptionString(context), + enabled: selectedType != type, + child: (selectedType == type) + ? SvgPicture.asset( + Assets.images.icCheck, + colorFilter: ColorFilter.mode( + context.colorScheme.primary, + BlendMode.srcATop, + ), + ) + : null, + onTap: () { + context.pop(); + onSelected.call(type); + }, + ), + ) + .toList(), + ); + } +} diff --git a/khelo/lib/ui/flow/tournament/add/add_tournament_view_model.dart b/khelo/lib/ui/flow/tournament/add/add_tournament_view_model.dart new file mode 100644 index 00000000..eaf67e47 --- /dev/null +++ b/khelo/lib/ui/flow/tournament/add/add_tournament_view_model.dart @@ -0,0 +1,176 @@ +import 'dart:io'; + +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:data/errors/app_error.dart'; +import 'package:data/service/file_upload/file_upload_service.dart'; +import 'package:data/service/tournament/tournament_service.dart'; +import 'package:data/storage/app_preferences.dart'; +import 'package:data/utils/constant/firebase_storage_constant.dart'; +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:khelo/domain/extensions/file_extension.dart'; + +part 'add_tournament_view_model.freezed.dart'; + +final addTournamentStateProvider = StateNotifierProvider.autoDispose< + AddTournamentViewNotifier, AddTournamentState>((ref) { + final notifier = AddTournamentViewNotifier( + ref.read(tournamentServiceProvider), + ref.read(fileUploadServiceProvider), + ref.read(currentUserPod)?.id, + ); + ref.listen(currentUserPod, (previous, next) { + notifier.setUserId(next?.id); + }); + return notifier; +}); + +class AddTournamentViewNotifier extends StateNotifier { + final FileUploadService _fileUploadService; + final TournamentService _tournamentService; + + AddTournamentViewNotifier( + this._tournamentService, + this._fileUploadService, + String? userId, + ) : super(AddTournamentState( + startDate: DateTime.now(), + endDate: DateTime.now().add(const Duration(days: 1)), + nameController: TextEditingController(), + currentUserId: userId, + )); + + void setUserId(String? userId) { + state = state.copyWith(currentUserId: userId); + } + + Future onImageChange({ + required String imagePath, + bool isBanner = false, + }) async { + try { + state = state.copyWith(imageUploading: true, actionError: null); + if (await File(imagePath).isFileUnderMaxSize()) { + isBanner + ? state = + state.copyWith(bannerPath: imagePath, imageUploading: false) + : state = + state.copyWith(profilePath: imagePath, imageUploading: false); + } else { + state = state.copyWith( + imageUploading: false, + actionError: const LargeAttachmentUploadError()); + } + } catch (e) { + state = state.copyWith(imageUploading: false, actionError: e); + debugPrint("AddTournamentViewNotifier: error while image upload -> $e"); + } + } + + void onChange() { + final name = state.nameController.text.trim(); + + state = state.copyWith(enableButton: name.isNotEmpty); + } + + void onSelectType(TournamentType type) { + state = state.copyWith(selectedType: type); + } + + void onStartDate(DateTime startDate) { + state = state.copyWith(startDate: startDate); + } + + void onEndDate(DateTime endDate) { + state = state.copyWith(endDate: endDate); + } + + void addTournament() async { + try { + if (state.currentUserId == null) return; + state = state.copyWith(loading: true, showDateError: false); + + if (state.endDate.isBefore(state.startDate)) { + state = state.copyWith(showDateError: true, loading: false); + return; + } + + final tournamentId = _tournamentService.generateTournamentId; + final name = state.nameController.text.trim(); + + if (state.profilePath != null) { + final profileImgUrl = await _fileUploadService.uploadProfileImage( + filePath: state.profilePath ?? '', + uploadPath: StorageConst.tournamentProfileUploadPath( + userId: state.currentUserId!, tournamentId: tournamentId), + ); + state = state.copyWith(profileImgUrl: profileImgUrl); + } + + if (state.bannerPath != null) { + final bannerImgUrl = await _fileUploadService.uploadProfileImage( + filePath: state.bannerPath ?? '', + uploadPath: StorageConst.tournamentBannerUploadPath( + userId: state.currentUserId!, tournamentId: tournamentId), + ); + state = state.copyWith(bannerImgUrl: bannerImgUrl); + } + + final tournament = TournamentModel( + id: tournamentId, + name: name, + type: state.selectedType, + start_date: state.startDate, + end_date: state.endDate, + created_at: DateTime.now(), + created_by: state.currentUserId!, + members: [ + TournamentMember( + id: state.currentUserId!, + role: TournamentMemberRole.organizer, + ), + ], + profile_img_url: state.profileImgUrl, + banner_img_url: state.bannerImgUrl, + ); + + await _tournamentService.createTournament(tournament); + + state = state.copyWith( + pop: true, loading: false, error: null, showDateError: false); + } catch (error) { + state = state.copyWith(loading: false, error: error); + debugPrint( + "AddTournamentViewNotifier: error while adding tournament -> $error"); + } + } + + @override + void dispose() { + state.nameController.dispose(); + super.dispose(); + } +} + +@freezed +class AddTournamentState with _$AddTournamentState { + const factory AddTournamentState({ + Object? error, + Object? actionError, + String? profilePath, + String? bannerPath, + String? currentUserId, + required DateTime endDate, + required DateTime startDate, + @Default(false) bool pop, + @Default(false) bool loading, + @Default(false) bool showDateError, + @Default(false) bool enableButton, + @Default(false) bool imageUploading, + @Default(null) String? profileImgUrl, + @Default(null) String? bannerImgUrl, + required TextEditingController nameController, + @Default(TournamentType.knockOut) TournamentType selectedType, + }) = _AddTournamentState; +} diff --git a/khelo/lib/ui/flow/tournament/add/add_tournament_view_model.freezed.dart b/khelo/lib/ui/flow/tournament/add/add_tournament_view_model.freezed.dart new file mode 100644 index 00000000..a55c612f --- /dev/null +++ b/khelo/lib/ui/flow/tournament/add/add_tournament_view_model.freezed.dart @@ -0,0 +1,474 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'add_tournament_view_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AddTournamentState { + Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; + String? get profilePath => throw _privateConstructorUsedError; + String? get bannerPath => throw _privateConstructorUsedError; + String? get currentUserId => throw _privateConstructorUsedError; + DateTime get endDate => throw _privateConstructorUsedError; + DateTime get startDate => throw _privateConstructorUsedError; + bool get pop => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; + bool get showDateError => throw _privateConstructorUsedError; + bool get enableButton => throw _privateConstructorUsedError; + bool get imageUploading => throw _privateConstructorUsedError; + String? get profileImgUrl => throw _privateConstructorUsedError; + String? get bannerImgUrl => throw _privateConstructorUsedError; + TextEditingController get nameController => + throw _privateConstructorUsedError; + TournamentType get selectedType => throw _privateConstructorUsedError; + + /// Create a copy of AddTournamentState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AddTournamentStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AddTournamentStateCopyWith<$Res> { + factory $AddTournamentStateCopyWith( + AddTournamentState value, $Res Function(AddTournamentState) then) = + _$AddTournamentStateCopyWithImpl<$Res, AddTournamentState>; + @useResult + $Res call( + {Object? error, + Object? actionError, + String? profilePath, + String? bannerPath, + String? currentUserId, + DateTime endDate, + DateTime startDate, + bool pop, + bool loading, + bool showDateError, + bool enableButton, + bool imageUploading, + String? profileImgUrl, + String? bannerImgUrl, + TextEditingController nameController, + TournamentType selectedType}); +} + +/// @nodoc +class _$AddTournamentStateCopyWithImpl<$Res, $Val extends AddTournamentState> + implements $AddTournamentStateCopyWith<$Res> { + _$AddTournamentStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AddTournamentState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = freezed, + Object? actionError = freezed, + Object? profilePath = freezed, + Object? bannerPath = freezed, + Object? currentUserId = freezed, + Object? endDate = null, + Object? startDate = null, + Object? pop = null, + Object? loading = null, + Object? showDateError = null, + Object? enableButton = null, + Object? imageUploading = null, + Object? profileImgUrl = freezed, + Object? bannerImgUrl = freezed, + Object? nameController = null, + Object? selectedType = null, + }) { + return _then(_value.copyWith( + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + profilePath: freezed == profilePath + ? _value.profilePath + : profilePath // ignore: cast_nullable_to_non_nullable + as String?, + bannerPath: freezed == bannerPath + ? _value.bannerPath + : bannerPath // ignore: cast_nullable_to_non_nullable + as String?, + currentUserId: freezed == currentUserId + ? _value.currentUserId + : currentUserId // ignore: cast_nullable_to_non_nullable + as String?, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + pop: null == pop + ? _value.pop + : pop // ignore: cast_nullable_to_non_nullable + as bool, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + showDateError: null == showDateError + ? _value.showDateError + : showDateError // ignore: cast_nullable_to_non_nullable + as bool, + enableButton: null == enableButton + ? _value.enableButton + : enableButton // ignore: cast_nullable_to_non_nullable + as bool, + imageUploading: null == imageUploading + ? _value.imageUploading + : imageUploading // ignore: cast_nullable_to_non_nullable + as bool, + profileImgUrl: freezed == profileImgUrl + ? _value.profileImgUrl + : profileImgUrl // ignore: cast_nullable_to_non_nullable + as String?, + bannerImgUrl: freezed == bannerImgUrl + ? _value.bannerImgUrl + : bannerImgUrl // ignore: cast_nullable_to_non_nullable + as String?, + nameController: null == nameController + ? _value.nameController + : nameController // ignore: cast_nullable_to_non_nullable + as TextEditingController, + selectedType: null == selectedType + ? _value.selectedType + : selectedType // ignore: cast_nullable_to_non_nullable + as TournamentType, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AddTournamentStateImplCopyWith<$Res> + implements $AddTournamentStateCopyWith<$Res> { + factory _$$AddTournamentStateImplCopyWith(_$AddTournamentStateImpl value, + $Res Function(_$AddTournamentStateImpl) then) = + __$$AddTournamentStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Object? error, + Object? actionError, + String? profilePath, + String? bannerPath, + String? currentUserId, + DateTime endDate, + DateTime startDate, + bool pop, + bool loading, + bool showDateError, + bool enableButton, + bool imageUploading, + String? profileImgUrl, + String? bannerImgUrl, + TextEditingController nameController, + TournamentType selectedType}); +} + +/// @nodoc +class __$$AddTournamentStateImplCopyWithImpl<$Res> + extends _$AddTournamentStateCopyWithImpl<$Res, _$AddTournamentStateImpl> + implements _$$AddTournamentStateImplCopyWith<$Res> { + __$$AddTournamentStateImplCopyWithImpl(_$AddTournamentStateImpl _value, + $Res Function(_$AddTournamentStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AddTournamentState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = freezed, + Object? actionError = freezed, + Object? profilePath = freezed, + Object? bannerPath = freezed, + Object? currentUserId = freezed, + Object? endDate = null, + Object? startDate = null, + Object? pop = null, + Object? loading = null, + Object? showDateError = null, + Object? enableButton = null, + Object? imageUploading = null, + Object? profileImgUrl = freezed, + Object? bannerImgUrl = freezed, + Object? nameController = null, + Object? selectedType = null, + }) { + return _then(_$AddTournamentStateImpl( + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + profilePath: freezed == profilePath + ? _value.profilePath + : profilePath // ignore: cast_nullable_to_non_nullable + as String?, + bannerPath: freezed == bannerPath + ? _value.bannerPath + : bannerPath // ignore: cast_nullable_to_non_nullable + as String?, + currentUserId: freezed == currentUserId + ? _value.currentUserId + : currentUserId // ignore: cast_nullable_to_non_nullable + as String?, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + pop: null == pop + ? _value.pop + : pop // ignore: cast_nullable_to_non_nullable + as bool, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + showDateError: null == showDateError + ? _value.showDateError + : showDateError // ignore: cast_nullable_to_non_nullable + as bool, + enableButton: null == enableButton + ? _value.enableButton + : enableButton // ignore: cast_nullable_to_non_nullable + as bool, + imageUploading: null == imageUploading + ? _value.imageUploading + : imageUploading // ignore: cast_nullable_to_non_nullable + as bool, + profileImgUrl: freezed == profileImgUrl + ? _value.profileImgUrl + : profileImgUrl // ignore: cast_nullable_to_non_nullable + as String?, + bannerImgUrl: freezed == bannerImgUrl + ? _value.bannerImgUrl + : bannerImgUrl // ignore: cast_nullable_to_non_nullable + as String?, + nameController: null == nameController + ? _value.nameController + : nameController // ignore: cast_nullable_to_non_nullable + as TextEditingController, + selectedType: null == selectedType + ? _value.selectedType + : selectedType // ignore: cast_nullable_to_non_nullable + as TournamentType, + )); + } +} + +/// @nodoc + +class _$AddTournamentStateImpl implements _AddTournamentState { + const _$AddTournamentStateImpl( + {this.error, + this.actionError, + this.profilePath, + this.bannerPath, + this.currentUserId, + required this.endDate, + required this.startDate, + this.pop = false, + this.loading = false, + this.showDateError = false, + this.enableButton = false, + this.imageUploading = false, + this.profileImgUrl = null, + this.bannerImgUrl = null, + required this.nameController, + this.selectedType = TournamentType.knockOut}); + + @override + final Object? error; + @override + final Object? actionError; + @override + final String? profilePath; + @override + final String? bannerPath; + @override + final String? currentUserId; + @override + final DateTime endDate; + @override + final DateTime startDate; + @override + @JsonKey() + final bool pop; + @override + @JsonKey() + final bool loading; + @override + @JsonKey() + final bool showDateError; + @override + @JsonKey() + final bool enableButton; + @override + @JsonKey() + final bool imageUploading; + @override + @JsonKey() + final String? profileImgUrl; + @override + @JsonKey() + final String? bannerImgUrl; + @override + final TextEditingController nameController; + @override + @JsonKey() + final TournamentType selectedType; + + @override + String toString() { + return 'AddTournamentState(error: $error, actionError: $actionError, profilePath: $profilePath, bannerPath: $bannerPath, currentUserId: $currentUserId, endDate: $endDate, startDate: $startDate, pop: $pop, loading: $loading, showDateError: $showDateError, enableButton: $enableButton, imageUploading: $imageUploading, profileImgUrl: $profileImgUrl, bannerImgUrl: $bannerImgUrl, nameController: $nameController, selectedType: $selectedType)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AddTournamentStateImpl && + const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError) && + (identical(other.profilePath, profilePath) || + other.profilePath == profilePath) && + (identical(other.bannerPath, bannerPath) || + other.bannerPath == bannerPath) && + (identical(other.currentUserId, currentUserId) || + other.currentUserId == currentUserId) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.pop, pop) || other.pop == pop) && + (identical(other.loading, loading) || other.loading == loading) && + (identical(other.showDateError, showDateError) || + other.showDateError == showDateError) && + (identical(other.enableButton, enableButton) || + other.enableButton == enableButton) && + (identical(other.imageUploading, imageUploading) || + other.imageUploading == imageUploading) && + (identical(other.profileImgUrl, profileImgUrl) || + other.profileImgUrl == profileImgUrl) && + (identical(other.bannerImgUrl, bannerImgUrl) || + other.bannerImgUrl == bannerImgUrl) && + (identical(other.nameController, nameController) || + other.nameController == nameController) && + (identical(other.selectedType, selectedType) || + other.selectedType == selectedType)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError), + profilePath, + bannerPath, + currentUserId, + endDate, + startDate, + pop, + loading, + showDateError, + enableButton, + imageUploading, + profileImgUrl, + bannerImgUrl, + nameController, + selectedType); + + /// Create a copy of AddTournamentState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AddTournamentStateImplCopyWith<_$AddTournamentStateImpl> get copyWith => + __$$AddTournamentStateImplCopyWithImpl<_$AddTournamentStateImpl>( + this, _$identity); +} + +abstract class _AddTournamentState implements AddTournamentState { + const factory _AddTournamentState( + {final Object? error, + final Object? actionError, + final String? profilePath, + final String? bannerPath, + final String? currentUserId, + required final DateTime endDate, + required final DateTime startDate, + final bool pop, + final bool loading, + final bool showDateError, + final bool enableButton, + final bool imageUploading, + final String? profileImgUrl, + final String? bannerImgUrl, + required final TextEditingController nameController, + final TournamentType selectedType}) = _$AddTournamentStateImpl; + + @override + Object? get error; + @override + Object? get actionError; + @override + String? get profilePath; + @override + String? get bannerPath; + @override + String? get currentUserId; + @override + DateTime get endDate; + @override + DateTime get startDate; + @override + bool get pop; + @override + bool get loading; + @override + bool get showDateError; + @override + bool get enableButton; + @override + bool get imageUploading; + @override + String? get profileImgUrl; + @override + String? get bannerImgUrl; + @override + TextEditingController get nameController; + @override + TournamentType get selectedType; + + /// Create a copy of AddTournamentState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AddTournamentStateImplCopyWith<_$AddTournamentStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart new file mode 100644 index 00000000..7c40e06a --- /dev/null +++ b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart @@ -0,0 +1,258 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:khelo/components/app_page.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/extensions/enum_extensions.dart'; +import 'package:khelo/domain/formatter/date_formatter.dart'; +import 'package:khelo/ui/flow/tournament/tournament_list_view_model.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicator/progress_indicator.dart'; +import 'package:style/text/app_text_style.dart'; + +import '../../../components/empty_screen.dart'; +import '../../../components/error_screen.dart'; +import '../../../gen/assets.gen.dart'; + +class TournamentListScreen extends ConsumerStatefulWidget { + const TournamentListScreen({super.key}); + + @override + ConsumerState createState() => + _TournamentListScreenState(); +} + +class _TournamentListScreenState extends ConsumerState + with WidgetsBindingObserver { + late TournamentListViewNotifier notifier; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + notifier = ref.read(tournamentListViewStateProvider.notifier); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.detached) { + // deallocate resources + notifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + + @override + Widget build(BuildContext context) { + return AppPage( + body: Builder( + builder: (context) => _body(context), + ), + ); + } + + Widget _body(BuildContext context) { + final state = ref.watch(tournamentListViewStateProvider); + if (state.loading) { + return const Center(child: AppProgressIndicator()); + } + if (state.error != null) { + return ErrorScreen( + error: state.error, + onRetryTap: notifier.loadTournaments, + ); + } + if (state.groupTournaments.isEmpty) { + return EmptyScreen( + title: context.l10n.tournament_list_empty_list_title, + description: context.l10n.tournament_list_empty_list_description, + isShowButton: false, + ); + } + + return _content(context, state); + } + + Widget _content(BuildContext context, TournamentListViewState state) { + return CustomScrollView( + slivers: [ + ..._tournaments(context, state), + SliverToBoxAdapter( + child: SizedBox(height: 16 + context.mediaQueryPadding.bottom), + ), + ], + ); + } + + List _tournaments( + BuildContext context, TournamentListViewState state) { + return List.generate(state.groupTournaments.length, (bookingIndex) { + final DateTime key = state.groupTournaments.keys.elementAt(bookingIndex); + final List tournaments = state.groupTournaments[key]!; + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: SliverAppbarDelegate( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + key.format(context, DateFormatType.monthYear), + style: AppTextStyle.header3 + .copyWith(color: context.colorScheme.textPrimary), + textScaler: TextScaler.noScaling, + ), + ), + ), + ), + SliverList.builder( + itemBuilder: (BuildContext context, int index) { + final tournament = tournaments[index]; + return _tournamentItem(context, tournament); + }, + itemCount: tournaments.length, + ), + ], + ); + }); + } + + Widget _tournamentItem(BuildContext context, TournamentModel tournament) { + return OnTapScale( + onTap: () {}, + child: Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: context.colorScheme.containerLow, + borderRadius: BorderRadius.circular(16)), + child: Row( + children: [ + _profileImage(context, tournament.profile_img_url), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tournament.name, + style: AppTextStyle.header4.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.tournament_list_match_title( + tournament.match_ids.length), + style: AppTextStyle.subtitle2.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + const SizedBox(height: 4), + _scheduleAndTypeView(context, tournament), + ], + ), + ), + const SizedBox(width: 16), + SvgPicture.asset(Assets.images.icArrowForward, + colorFilter: ColorFilter.mode( + context.colorScheme.textDisabled, + BlendMode.srcATop, + )) + ], + ), + ), + ); + } + + Widget _profileImage(BuildContext context, String? imageUrl) { + return Container( + height: 82, + width: 82, + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(6), + image: (imageUrl != null) + ? DecorationImage( + image: CachedNetworkImageProvider(imageUrl), + fit: BoxFit.cover, + ) + : null), + child: imageUrl == null + ? Center( + child: SvgPicture.asset(Assets.images.icTournaments, + height: 32, + width: 32, + colorFilter: ColorFilter.mode( + context.colorScheme.textSecondary, + BlendMode.srcATop, + )), + ) + : null, + ); + } + + Widget _scheduleAndTypeView( + BuildContext context, + 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), + style: AppTextStyle.caption + .copyWith(color: context.colorScheme.textDisabled), + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 5, + width: 5, + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.textSecondary, + ), + )), + TextSpan(text: tournament.type.getString(context)) + ])); + } +} + +class SliverAppbarDelegate extends SliverPersistentHeaderDelegate { + final Widget child; + + SliverAppbarDelegate({Key? key, required this.child}); + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Container( + color: context.colorScheme.surface, + alignment: Alignment.centerLeft, + child: child, + ); + } + + @override + bool shouldRebuild(SliverAppbarDelegate oldDelegate) { + return child != oldDelegate.child; + } + + @override + double get maxExtent => 50; + + @override + double get minExtent => 50; +} diff --git a/khelo/lib/ui/flow/tournament/tournament_list_view_model.dart b/khelo/lib/ui/flow/tournament/tournament_list_view_model.dart new file mode 100644 index 00000000..5b04c615 --- /dev/null +++ b/khelo/lib/ui/flow/tournament/tournament_list_view_model.dart @@ -0,0 +1,85 @@ +import 'dart:async'; +import 'package:collection/collection.dart'; +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:data/service/tournament/tournament_service.dart'; +import 'package:data/storage/app_preferences.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:style/extensions/date_extensions.dart'; + +part 'tournament_list_view_model.freezed.dart'; + +final tournamentListViewStateProvider = + StateNotifierProvider( + (ref) { + final notifier = TournamentListViewNotifier( + ref.read(tournamentServiceProvider), + ref.read(currentUserPod)?.id, + ); + ref.listen(currentUserPod, (previous, next) { + notifier._setUserId(next?.id); + }); + return notifier; + }, +); + +class TournamentListViewNotifier + extends StateNotifier { + final TournamentService _tournamentService; + StreamSubscription? _tournamentStreamSubscription; + + TournamentListViewNotifier(this._tournamentService, String? userId) + : super(TournamentListViewState(currentUserId: userId)) { + loadTournaments(); + } + + void _setUserId(String? userId) { + if (userId == null) { + _tournamentStreamSubscription?.cancel(); + } + state = state.copyWith(currentUserId: userId); + loadTournaments(); + } + + Future loadTournaments() async { + if (state.currentUserId == null) return; + _tournamentStreamSubscription?.cancel(); + state = state.copyWith(loading: true); + _tournamentStreamSubscription = _tournamentService + .streamCurrentUserRelatedMatches(state.currentUserId!) + .listen((tournaments) { + final groupTournaments = _groupTournaments(tournaments); + state = state.copyWith( + groupTournaments: groupTournaments, loading: false, error: null); + }, onError: (e) { + state = state.copyWith(loading: false, error: e); + debugPrint( + "TournamentListViewNotifier: error while loading tournament list -> $e"); + }); + } + + Map> _groupTournaments( + List tournaments) { + return groupBy( + tournaments, + (tournament) => tournament.start_date.startOfMonth, + ); + } + + @override + void dispose() { + _tournamentStreamSubscription?.cancel(); + super.dispose(); + } +} + +@freezed +class TournamentListViewState with _$TournamentListViewState { + const factory TournamentListViewState({ + String? currentUserId, + Object? error, + @Default(true) bool loading, + @Default({}) Map> groupTournaments, + }) = _TournamentListViewState; +} diff --git a/khelo/lib/ui/flow/tournament/tournament_list_view_model.freezed.dart b/khelo/lib/ui/flow/tournament/tournament_list_view_model.freezed.dart new file mode 100644 index 00000000..df63d56a --- /dev/null +++ b/khelo/lib/ui/flow/tournament/tournament_list_view_model.freezed.dart @@ -0,0 +1,223 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'tournament_list_view_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$TournamentListViewState { + String? get currentUserId => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; + Map> get groupTournaments => + throw _privateConstructorUsedError; + + /// Create a copy of TournamentListViewState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TournamentListViewStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TournamentListViewStateCopyWith<$Res> { + factory $TournamentListViewStateCopyWith(TournamentListViewState value, + $Res Function(TournamentListViewState) then) = + _$TournamentListViewStateCopyWithImpl<$Res, TournamentListViewState>; + @useResult + $Res call( + {String? currentUserId, + Object? error, + bool loading, + Map> groupTournaments}); +} + +/// @nodoc +class _$TournamentListViewStateCopyWithImpl<$Res, + $Val extends TournamentListViewState> + implements $TournamentListViewStateCopyWith<$Res> { + _$TournamentListViewStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TournamentListViewState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentUserId = freezed, + Object? error = freezed, + Object? loading = null, + Object? groupTournaments = null, + }) { + return _then(_value.copyWith( + currentUserId: freezed == currentUserId + ? _value.currentUserId + : currentUserId // ignore: cast_nullable_to_non_nullable + as String?, + error: freezed == error ? _value.error : error, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + groupTournaments: null == groupTournaments + ? _value.groupTournaments + : groupTournaments // ignore: cast_nullable_to_non_nullable + as Map>, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TournamentListViewStateImplCopyWith<$Res> + implements $TournamentListViewStateCopyWith<$Res> { + factory _$$TournamentListViewStateImplCopyWith( + _$TournamentListViewStateImpl value, + $Res Function(_$TournamentListViewStateImpl) then) = + __$$TournamentListViewStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? currentUserId, + Object? error, + bool loading, + Map> groupTournaments}); +} + +/// @nodoc +class __$$TournamentListViewStateImplCopyWithImpl<$Res> + extends _$TournamentListViewStateCopyWithImpl<$Res, + _$TournamentListViewStateImpl> + implements _$$TournamentListViewStateImplCopyWith<$Res> { + __$$TournamentListViewStateImplCopyWithImpl( + _$TournamentListViewStateImpl _value, + $Res Function(_$TournamentListViewStateImpl) _then) + : super(_value, _then); + + /// Create a copy of TournamentListViewState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentUserId = freezed, + Object? error = freezed, + Object? loading = null, + Object? groupTournaments = null, + }) { + return _then(_$TournamentListViewStateImpl( + currentUserId: freezed == currentUserId + ? _value.currentUserId + : currentUserId // ignore: cast_nullable_to_non_nullable + as String?, + error: freezed == error ? _value.error : error, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + groupTournaments: null == groupTournaments + ? _value._groupTournaments + : groupTournaments // ignore: cast_nullable_to_non_nullable + as Map>, + )); + } +} + +/// @nodoc + +class _$TournamentListViewStateImpl implements _TournamentListViewState { + const _$TournamentListViewStateImpl( + {this.currentUserId, + this.error, + this.loading = true, + final Map> groupTournaments = const {}}) + : _groupTournaments = groupTournaments; + + @override + final String? currentUserId; + @override + final Object? error; + @override + @JsonKey() + final bool loading; + final Map> _groupTournaments; + @override + @JsonKey() + Map> get groupTournaments { + if (_groupTournaments is EqualUnmodifiableMapView) return _groupTournaments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_groupTournaments); + } + + @override + String toString() { + return 'TournamentListViewState(currentUserId: $currentUserId, error: $error, loading: $loading, groupTournaments: $groupTournaments)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TournamentListViewStateImpl && + (identical(other.currentUserId, currentUserId) || + other.currentUserId == currentUserId) && + const DeepCollectionEquality().equals(other.error, error) && + (identical(other.loading, loading) || other.loading == loading) && + const DeepCollectionEquality() + .equals(other._groupTournaments, _groupTournaments)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + currentUserId, + const DeepCollectionEquality().hash(error), + loading, + const DeepCollectionEquality().hash(_groupTournaments)); + + /// Create a copy of TournamentListViewState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TournamentListViewStateImplCopyWith<_$TournamentListViewStateImpl> + get copyWith => __$$TournamentListViewStateImplCopyWithImpl< + _$TournamentListViewStateImpl>(this, _$identity); +} + +abstract class _TournamentListViewState implements TournamentListViewState { + const factory _TournamentListViewState( + {final String? currentUserId, + final Object? error, + final bool loading, + final Map> groupTournaments}) = + _$TournamentListViewStateImpl; + + @override + String? get currentUserId; + @override + Object? get error; + @override + bool get loading; + @override + Map> get groupTournaments; + + /// Create a copy of TournamentListViewState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TournamentListViewStateImplCopyWith<_$TournamentListViewStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/style/lib/button/action_button.dart b/style/lib/button/action_button.dart index 1cae9d9c..854fac3d 100644 --- a/style/lib/button/action_button.dart +++ b/style/lib/button/action_button.dart @@ -8,6 +8,7 @@ Widget actionButton( void Function()? onPressed, required Widget icon, EdgeInsets padding = EdgeInsets.zero, + bool shrinkWrap = false, }) { if (Platform.isIOS) { return CupertinoButton( @@ -20,6 +21,11 @@ Widget actionButton( onPressed: onPressed, icon: icon, padding: padding, + style: ButtonStyle( + tapTargetSize: shrinkWrap + ? MaterialTapTargetSize.shrinkWrap + : MaterialTapTargetSize.padded, + ), ); } } diff --git a/style/lib/extensions/date_extensions.dart b/style/lib/extensions/date_extensions.dart index fa7b7a8f..f6f75f26 100644 --- a/style/lib/extensions/date_extensions.dart +++ b/style/lib/extensions/date_extensions.dart @@ -1,3 +1,5 @@ extension DateTimeExtensions on DateTime { DateTime get startOfDay => DateTime(year, month, day); + + DateTime get startOfMonth => DateTime(year, month, 1); } diff --git a/style/lib/pickers/date_and_time_picker.dart b/style/lib/pickers/date_and_time_picker.dart new file mode 100644 index 00000000..4357d614 --- /dev/null +++ b/style/lib/pickers/date_and_time_picker.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:style/extensions/context_extensions.dart'; + +import '../theme/colors.dart'; + +Future selectDate( + BuildContext context, { + String? helpText, + required DateTime initialDate, + required Function(DateTime) onDateSelected, +}) async { + showDatePicker( + context: context, + helpText: helpText, + initialDate: initialDate, + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 1), + builder: (context, child) { + return Theme( + data: context.brightness == Brightness.dark + ? materialThemeDataDark + : materialThemeDataLight, + child: child!, + ); + }, + ).then((selectedDate) { + if (selectedDate != null) { + DateTime selectedDateTime = DateTime( + selectedDate.year, + selectedDate.month, + selectedDate.day, + initialDate.hour, + initialDate.minute, + ); + onDateSelected.call(selectedDateTime); + } + }); +} + +Future selectTime( + BuildContext context, { + required DateTime initialTime, + required Function(DateTime) onTimeSelected, +}) async { + showTimePicker( + context: context, + initialTime: TimeOfDay( + hour: initialTime.hour, + minute: initialTime.minute, + ), + builder: (context, child) { + return Theme( + data: context.brightness == Brightness.dark + ? materialThemeDataDark + : materialThemeDataLight, + child: child!, + ); + }, + ).then((selectedTime) { + if (selectedTime != null) { + DateTime selectedDateTime = DateTime( + initialTime.year, + initialTime.month, + initialTime.day, + selectedTime.hour, + selectedTime.minute, + ); + onTimeSelected.call(selectedDateTime); + } + }); +}