From 6e6f248702a9fd13d4c42527d5eafc5ba1a5c960 Mon Sep 17 00:00:00 2001 From: llsajjad Date: Sun, 21 Sep 2025 14:03:32 +0330 Subject: [PATCH] Fixed pagination tenders for desktop and added new config for notification --- android/app/build.gradle.kts | 2 +- android/app/google-services.json | 10 +- .../tenders/tenders_data/tenders_data.dart | 1 - .../tenders_data/tenders_data.freezed.dart | 71 ++-- .../tenders/tenders_data/tenders_data.g.dart | 9 +- .../tenders_response/tenders_response.dart | 2 + .../tenders_response.freezed.dart | 71 ++-- .../tenders_response/tenders_response.g.dart | 5 + lib/gen/assets.gen.dart | 12 + lib/main.dart | 16 +- lib/view_models/tenders_view_model.dart | 41 +++ lib/views/tenders/pages/d_tenders_page.dart | 312 ++++++++++++------ .../tenders/strings/tenders_strings.dart | 2 + .../tenders/widgets/main_tenders_slider.dart | 73 +++- web/firebase-messaging-sw.js | 14 +- 15 files changed, 422 insertions(+), 219 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7b4b3c7..1d269f8 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -25,7 +25,7 @@ android { applicationId = "com.opplens.app" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/app/google-services.json b/android/app/google-services.json index 03e737b..6766643 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -1,13 +1,13 @@ { "project_info": { - "project_number": "1071558853812", - "project_id": "opplens", - "storage_bucket": "opplens.firebasestorage.app" + "project_number": "6923326255", + "project_id": "opplens-270d1", + "storage_bucket": "opplens-270d1.firebasestorage.app" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:1071558853812:android:7831119a56a99c5948c3e1", + "mobilesdk_app_id": "1:6923326255:android:6704c5b71e4451abed87da", "android_client_info": { "package_name": "com.opplens.app" } @@ -15,7 +15,7 @@ "oauth_client": [], "api_key": [ { - "current_key": "AIzaSyD5LaGXppjGEdTf-a0Ha25FX1p0v_h-c8A" + "current_key": "AIzaSyCqZ6B5iih0RdFj0lf5_erwbZvMyb7obEg" } ], "services": { diff --git a/lib/data/services/model/tenders/tenders_data/tenders_data.dart b/lib/data/services/model/tenders/tenders_data/tenders_data.dart index 9e046ea..f3f0082 100644 --- a/lib/data/services/model/tenders/tenders_data/tenders_data.dart +++ b/lib/data/services/model/tenders/tenders_data/tenders_data.dart @@ -12,7 +12,6 @@ part 'tenders_data.g.dart'; abstract class TendersData with _$TendersData { const factory TendersData({ required List? tenders, - required Meta? metadata, }) = _TendersData; factory TendersData.fromJson(Map json) => diff --git a/lib/data/services/model/tenders/tenders_data/tenders_data.freezed.dart b/lib/data/services/model/tenders/tenders_data/tenders_data.freezed.dart index 77d49f3..eb48288 100644 --- a/lib/data/services/model/tenders/tenders_data/tenders_data.freezed.dart +++ b/lib/data/services/model/tenders/tenders_data/tenders_data.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$TendersData { - List? get tenders; Meta? get metadata; + List? get tenders; /// Create a copy of TendersData /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $TendersDataCopyWith get copyWith => _$TendersDataCopyWithImpl Object.hash(runtimeType,const DeepCollectionEquality().hash(tenders),metadata); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(tenders)); @override String toString() { - return 'TendersData(tenders: $tenders, metadata: $metadata)'; + return 'TendersData(tenders: $tenders)'; } @@ -48,11 +48,11 @@ abstract mixin class $TendersDataCopyWith<$Res> { factory $TendersDataCopyWith(TendersData value, $Res Function(TendersData) _then) = _$TendersDataCopyWithImpl; @useResult $Res call({ - List? tenders, Meta? metadata + List? tenders }); -$MetaCopyWith<$Res>? get metadata; + } /// @nodoc @@ -65,26 +65,13 @@ class _$TendersDataCopyWithImpl<$Res> /// Create a copy of TendersData /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? tenders = freezed,Object? metadata = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? tenders = freezed,}) { return _then(_self.copyWith( tenders: freezed == tenders ? _self.tenders : tenders // ignore: cast_nullable_to_non_nullable -as List?,metadata: freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable -as Meta?, +as List?, )); } -/// Create a copy of TendersData -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$MetaCopyWith<$Res>? get metadata { - if (_self.metadata == null) { - return null; - } - return $MetaCopyWith<$Res>(_self.metadata!, (value) { - return _then(_self.copyWith(metadata: value)); - }); -} } @@ -166,10 +153,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List? tenders, Meta? metadata)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List? tenders)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _TendersData() when $default != null: -return $default(_that.tenders,_that.metadata);case _: +return $default(_that.tenders);case _: return orElse(); } @@ -187,10 +174,10 @@ return $default(_that.tenders,_that.metadata);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List? tenders, Meta? metadata) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List? tenders) $default,) {final _that = this; switch (_that) { case _TendersData(): -return $default(_that.tenders,_that.metadata);case _: +return $default(_that.tenders);case _: throw StateError('Unexpected subclass'); } @@ -207,10 +194,10 @@ return $default(_that.tenders,_that.metadata);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? tenders, Meta? metadata)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? tenders)? $default,) {final _that = this; switch (_that) { case _TendersData() when $default != null: -return $default(_that.tenders,_that.metadata);case _: +return $default(_that.tenders);case _: return null; } @@ -222,7 +209,7 @@ return $default(_that.tenders,_that.metadata);case _: @JsonSerializable() class _TendersData implements TendersData { - const _TendersData({required final List? tenders, required this.metadata}): _tenders = tenders; + const _TendersData({required final List? tenders}): _tenders = tenders; factory _TendersData.fromJson(Map json) => _$TendersDataFromJson(json); final List? _tenders; @@ -234,7 +221,6 @@ class _TendersData implements TendersData { return EqualUnmodifiableListView(value); } -@override final Meta? metadata; /// Create a copy of TendersData /// with the given fields replaced by the non-null parameter values. @@ -249,16 +235,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _TendersData&&const DeepCollectionEquality().equals(other._tenders, _tenders)&&(identical(other.metadata, metadata) || other.metadata == metadata)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TendersData&&const DeepCollectionEquality().equals(other._tenders, _tenders)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_tenders),metadata); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_tenders)); @override String toString() { - return 'TendersData(tenders: $tenders, metadata: $metadata)'; + return 'TendersData(tenders: $tenders)'; } @@ -269,11 +255,11 @@ abstract mixin class _$TendersDataCopyWith<$Res> implements $TendersDataCopyWith factory _$TendersDataCopyWith(_TendersData value, $Res Function(_TendersData) _then) = __$TendersDataCopyWithImpl; @override @useResult $Res call({ - List? tenders, Meta? metadata + List? tenders }); -@override $MetaCopyWith<$Res>? get metadata; + } /// @nodoc @@ -286,27 +272,14 @@ class __$TendersDataCopyWithImpl<$Res> /// Create a copy of TendersData /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? tenders = freezed,Object? metadata = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? tenders = freezed,}) { return _then(_TendersData( tenders: freezed == tenders ? _self._tenders : tenders // ignore: cast_nullable_to_non_nullable -as List?,metadata: freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable -as Meta?, +as List?, )); } -/// Create a copy of TendersData -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$MetaCopyWith<$Res>? get metadata { - if (_self.metadata == null) { - return null; - } - return $MetaCopyWith<$Res>(_self.metadata!, (value) { - return _then(_self.copyWith(metadata: value)); - }); -} } // dart format on diff --git a/lib/data/services/model/tenders/tenders_data/tenders_data.g.dart b/lib/data/services/model/tenders/tenders_data/tenders_data.g.dart index 736dd48..4f8f13b 100644 --- a/lib/data/services/model/tenders/tenders_data/tenders_data.g.dart +++ b/lib/data/services/model/tenders/tenders_data/tenders_data.g.dart @@ -11,14 +11,7 @@ _TendersData _$TendersDataFromJson(Map json) => _TendersData( (json['tenders'] as List?) ?.map((e) => TenderData.fromJson(e as Map)) .toList(), - metadata: - json['metadata'] == null - ? null - : Meta.fromJson(json['metadata'] as Map), ); Map _$TendersDataToJson(_TendersData instance) => - { - 'tenders': instance.tenders, - 'metadata': instance.metadata, - }; + {'tenders': instance.tenders}; diff --git a/lib/data/services/model/tenders/tenders_response/tenders_response.dart b/lib/data/services/model/tenders/tenders_response/tenders_response.dart index cd8a356..a3eaf95 100644 --- a/lib/data/services/model/tenders/tenders_response/tenders_response.dart +++ b/lib/data/services/model/tenders/tenders_response/tenders_response.dart @@ -1,6 +1,7 @@ // ignore_for_file: invalid_annotation_target import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:tm_app/data/services/model/meta/meta.dart'; import '../../error/error_model.dart'; import '../tenders_data/tenders_data.dart'; @@ -15,6 +16,7 @@ abstract class TendersResponse with _$TendersResponse { required ErrorModel? error, required String? message, required bool? success, + required Meta? meta, }) = _TendersResponse; factory TendersResponse.fromJson(Map json) => diff --git a/lib/data/services/model/tenders/tenders_response/tenders_response.freezed.dart b/lib/data/services/model/tenders/tenders_response/tenders_response.freezed.dart index 0ad9ac4..3e67fbc 100644 --- a/lib/data/services/model/tenders/tenders_response/tenders_response.freezed.dart +++ b/lib/data/services/model/tenders/tenders_response/tenders_response.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$TendersResponse { - TendersData? get data; ErrorModel? get error; String? get message; bool? get success; + TendersData? get data; ErrorModel? get error; String? get message; bool? get success; Meta? get meta; /// Create a copy of TendersResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $TendersResponseCopyWith get copyWith => _$TendersResponseCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is TendersResponse&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is TendersResponse&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)&&(identical(other.meta, meta) || other.meta == meta)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,data,error,message,success); +int get hashCode => Object.hash(runtimeType,data,error,message,success,meta); @override String toString() { - return 'TendersResponse(data: $data, error: $error, message: $message, success: $success)'; + return 'TendersResponse(data: $data, error: $error, message: $message, success: $success, meta: $meta)'; } @@ -48,11 +48,11 @@ abstract mixin class $TendersResponseCopyWith<$Res> { factory $TendersResponseCopyWith(TendersResponse value, $Res Function(TendersResponse) _then) = _$TendersResponseCopyWithImpl; @useResult $Res call({ - TendersData? data, ErrorModel? error, String? message, bool? success + TendersData? data, ErrorModel? error, String? message, bool? success, Meta? meta }); -$TendersDataCopyWith<$Res>? get data;$ErrorModelCopyWith<$Res>? get error; +$TendersDataCopyWith<$Res>? get data;$ErrorModelCopyWith<$Res>? get error;$MetaCopyWith<$Res>? get meta; } /// @nodoc @@ -65,13 +65,14 @@ class _$TendersResponseCopyWithImpl<$Res> /// Create a copy of TendersResponse /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,Object? meta = freezed,}) { return _then(_self.copyWith( data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as TendersData?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as bool?, +as bool?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable +as Meta?, )); } /// Create a copy of TendersResponse @@ -98,6 +99,18 @@ $ErrorModelCopyWith<$Res>? get error { return $ErrorModelCopyWith<$Res>(_self.error!, (value) { return _then(_self.copyWith(error: value)); }); +}/// Create a copy of TendersResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MetaCopyWith<$Res>? get meta { + if (_self.meta == null) { + return null; + } + + return $MetaCopyWith<$Res>(_self.meta!, (value) { + return _then(_self.copyWith(meta: value)); + }); } } @@ -180,10 +193,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( TendersData? data, ErrorModel? error, String? message, bool? success)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( TendersData? data, ErrorModel? error, String? message, bool? success, Meta? meta)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _TendersResponse() when $default != null: -return $default(_that.data,_that.error,_that.message,_that.success);case _: +return $default(_that.data,_that.error,_that.message,_that.success,_that.meta);case _: return orElse(); } @@ -201,10 +214,10 @@ return $default(_that.data,_that.error,_that.message,_that.success);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( TendersData? data, ErrorModel? error, String? message, bool? success) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( TendersData? data, ErrorModel? error, String? message, bool? success, Meta? meta) $default,) {final _that = this; switch (_that) { case _TendersResponse(): -return $default(_that.data,_that.error,_that.message,_that.success);case _: +return $default(_that.data,_that.error,_that.message,_that.success,_that.meta);case _: throw StateError('Unexpected subclass'); } @@ -221,10 +234,10 @@ return $default(_that.data,_that.error,_that.message,_that.success);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( TendersData? data, ErrorModel? error, String? message, bool? success)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( TendersData? data, ErrorModel? error, String? message, bool? success, Meta? meta)? $default,) {final _that = this; switch (_that) { case _TendersResponse() when $default != null: -return $default(_that.data,_that.error,_that.message,_that.success);case _: +return $default(_that.data,_that.error,_that.message,_that.success,_that.meta);case _: return null; } @@ -236,13 +249,14 @@ return $default(_that.data,_that.error,_that.message,_that.success);case _: @JsonSerializable() class _TendersResponse implements TendersResponse { - const _TendersResponse({required this.data, required this.error, required this.message, required this.success}); + const _TendersResponse({required this.data, required this.error, required this.message, required this.success, required this.meta}); factory _TendersResponse.fromJson(Map json) => _$TendersResponseFromJson(json); @override final TendersData? data; @override final ErrorModel? error; @override final String? message; @override final bool? success; +@override final Meta? meta; /// Create a copy of TendersResponse /// with the given fields replaced by the non-null parameter values. @@ -257,16 +271,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _TendersResponse&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TendersResponse&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)&&(identical(other.meta, meta) || other.meta == meta)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,data,error,message,success); +int get hashCode => Object.hash(runtimeType,data,error,message,success,meta); @override String toString() { - return 'TendersResponse(data: $data, error: $error, message: $message, success: $success)'; + return 'TendersResponse(data: $data, error: $error, message: $message, success: $success, meta: $meta)'; } @@ -277,11 +291,11 @@ abstract mixin class _$TendersResponseCopyWith<$Res> implements $TendersResponse factory _$TendersResponseCopyWith(_TendersResponse value, $Res Function(_TendersResponse) _then) = __$TendersResponseCopyWithImpl; @override @useResult $Res call({ - TendersData? data, ErrorModel? error, String? message, bool? success + TendersData? data, ErrorModel? error, String? message, bool? success, Meta? meta }); -@override $TendersDataCopyWith<$Res>? get data;@override $ErrorModelCopyWith<$Res>? get error; +@override $TendersDataCopyWith<$Res>? get data;@override $ErrorModelCopyWith<$Res>? get error;@override $MetaCopyWith<$Res>? get meta; } /// @nodoc @@ -294,13 +308,14 @@ class __$TendersResponseCopyWithImpl<$Res> /// Create a copy of TendersResponse /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,Object? meta = freezed,}) { return _then(_TendersResponse( data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as TendersData?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as bool?, +as bool?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable +as Meta?, )); } @@ -328,6 +343,18 @@ $ErrorModelCopyWith<$Res>? get error { return $ErrorModelCopyWith<$Res>(_self.error!, (value) { return _then(_self.copyWith(error: value)); }); +}/// Create a copy of TendersResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MetaCopyWith<$Res>? get meta { + if (_self.meta == null) { + return null; + } + + return $MetaCopyWith<$Res>(_self.meta!, (value) { + return _then(_self.copyWith(meta: value)); + }); } } diff --git a/lib/data/services/model/tenders/tenders_response/tenders_response.g.dart b/lib/data/services/model/tenders/tenders_response/tenders_response.g.dart index c5d3ccd..c023023 100644 --- a/lib/data/services/model/tenders/tenders_response/tenders_response.g.dart +++ b/lib/data/services/model/tenders/tenders_response/tenders_response.g.dart @@ -18,6 +18,10 @@ _TendersResponse _$TendersResponseFromJson(Map json) => : ErrorModel.fromJson(json['error'] as Map), message: json['message'] as String?, success: json['success'] as bool?, + meta: + json['meta'] == null + ? null + : Meta.fromJson(json['meta'] as Map), ); Map _$TendersResponseToJson(_TendersResponse instance) => @@ -26,4 +30,5 @@ Map _$TendersResponseToJson(_TendersResponse instance) => 'error': instance.error, 'message': instance.message, 'success': instance.success, + 'meta': instance.meta, }; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 935ff91..06158cb 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -80,6 +80,9 @@ class $AssetsIconsGen { /// File path: assets/icons/icon_contracts.svg String get iconContracts => 'assets/icons/icon_contracts.svg'; + /// File path: assets/icons/left-arrow.svg + String get leftArrow => 'assets/icons/left-arrow.svg'; + /// File path: assets/icons/like.svg String get like => 'assets/icons/like.svg'; @@ -113,9 +116,15 @@ class $AssetsIconsGen { /// File path: assets/icons/profile-circle_active.svg String get profileCircleActive => 'assets/icons/profile-circle_active.svg'; + /// File path: assets/icons/search_normal.svg + String get searchNormal => 'assets/icons/search_normal.svg'; + /// File path: assets/icons/shield.svg String get shield => 'assets/icons/shield.svg'; + /// File path: assets/icons/sort.svg + String get sort => 'assets/icons/sort.svg'; + /// File path: assets/icons/sun.svg String get sun => 'assets/icons/sun.svg'; @@ -180,6 +189,7 @@ class $AssetsIconsGen { home, homeActive, iconContracts, + leftArrow, like, location, logoBig, @@ -191,7 +201,9 @@ class $AssetsIconsGen { notifyIconActive, profileCircle, profileCircleActive, + searchNormal, shield, + sort, sun, taskSquareActive, taskSquare, diff --git a/lib/main.dart b/lib/main.dart index bdc3c9e..3380f0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,7 +37,7 @@ void main() async { final prefs = await SharedPreferences.getInstance(); List providers = [ - Provider(create: (context) => prefs), + Provider(create: (context) => prefs), ...providersRemote, ]; // Configure web URL strategy if running on web @@ -46,13 +46,13 @@ void main() async { if (kIsWeb) { await Firebase.initializeApp( options: const FirebaseOptions( - apiKey: 'AIzaSyAUQhM789-0qZIaIWtwTQXyz9gEtN-TnnU', - authDomain: 'opplens.firebaseapp.com', - appId: '1:1071558853812:web:d9ca896b088ee7da48c3e1', - messagingSenderId: '1071558853812', - projectId: 'opplens', - storageBucket: 'opplens.firebasestorage.app', - measurementId: 'G-GJGP9WB28B', + apiKey: 'AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0', + authDomain: 'opplens-270d1.firebaseapp.com', + projectId: 'opplens-270d1', + storageBucket: 'opplens-270d1.firebasestorage.app', + messagingSenderId: '6923326255', + appId: '1:6923326255:web:c71763ece06aaaf3ed87da', + measurementId: 'G-M5HQR4YZVS', ), ); final _firebaseMessaging = FirebaseMessaging.instance; diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index 8270978..9f4fc59 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -54,6 +54,8 @@ class TendersViewModel with ChangeNotifier { int currentPage = 1; int _currentOffset = 0; + int _totalPages = 0; + int get totalPages => _totalPages; static const int _pageSize = 10; bool _hasMoreData = true; @@ -117,6 +119,14 @@ class TendersViewModel with ChangeNotifier { getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!); getTenderApprovals(_tendersResponse!.data!.tenders!.first.id!); } + if (_tendersResponse?.meta?.pages != null) { + _totalPages = _tendersResponse!.meta!.pages!; + print('Total pages: $_totalPages'); + } else { + _totalPages = 1; + print('Total pages not available, fallback to 1'); + } + notifyListeners(); break; case Error(): _errorMessage = result.error.toString(); @@ -500,4 +510,35 @@ class TendersViewModel with ChangeNotifier { _tenderApprovalErrorMessage = null; notifyListeners(); } + +void jumpToPage(int page) async { + final meta = tendersResponse?.meta; + if (meta?.pages == null) { + return; + } + if (page < 1 || page > meta!.pages!) { + return; + } + + currentPage = page; + _currentOffset = (page - 1) * _pageSize; + + final result = await _tendersRepository.getTenders( + limit: _pageSize, + offset: _currentOffset, + ); + + switch (result) { + case Ok(): + _tendersResponse = result.value; + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + + notifyListeners(); +} + + } diff --git a/lib/views/tenders/pages/d_tenders_page.dart b/lib/views/tenders/pages/d_tenders_page.dart index 6ce2c02..0dff86c 100644 --- a/lib/views/tenders/pages/d_tenders_page.dart +++ b/lib/views/tenders/pages/d_tenders_page.dart @@ -7,6 +7,8 @@ import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; +import 'package:tm_app/views/shared/page_selection_dialog.dart'; +import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; import '../../../core/constants/common_strings.dart'; @@ -21,6 +23,7 @@ class DesktopTendersPage extends StatefulWidget { class _DesktopTendersPageState extends State { late final TendersViewModel viewModel; + @override void initState() { super.initState(); @@ -49,121 +52,49 @@ class _DesktopTendersPageState extends State { backgroundColor: AppColors.backgroundColor, body: Column( children: [ - const DesktopNavigationWidget( - currentIndex: 1, // Tenders index - ), - + const DesktopNavigationWidget(currentIndex: 1), SizedBox(height: 48.0.h()), - //search box - SizedBox( - height: 48.0.h(), - width: 680, - child: TextField( - style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey0), - decoration: InputDecoration( - hintText: 'Search', - hintStyle: TextStyle( - fontSize: 16.0.sp(), - color: AppColors.grey60, - ), - prefixIcon: Padding( - padding: EdgeInsets.all(12.0.w()), - child: SvgPicture.asset( - AssetsManager.normalSearch, - width: 24.0.w(), - height: 24.0.h(), - ), - ), - filled: true, - fillColor: AppColors.grey0, - contentPadding: EdgeInsets.symmetric( - horizontal: 12.0.w(), - vertical: 12.0.h(), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - borderSide: const BorderSide( - color: AppColors.mainBlue, - width: 1, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - borderSide: BorderSide(color: AppColors.grey40, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - borderSide: const BorderSide( - color: AppColors.grey, - width: 1.5, - ), - ), - ), - ), - ), - Container( - margin: EdgeInsets.only(top: 25.0.h()), - height: 40.0.h(), - width: 680, - child: Row( - children: [ - BaseButton( - width: 178.0.w(), - isEnabled: true, - onPressed: () {}, - text: 'Filter', - icon: AssetsManager.filter, - iconColor: AppColors.textBlue, - backgroundColor: AppColors.primary2, - textColor: AppColors.textBlue, - ), - SizedBox(width: 10.0.w()), - BaseButton( - width: 178.0.w(), - isEnabled: true, - onPressed: () {}, - text: 'Sort', - icon: AssetsManager.sort, - iconColor: AppColors.textBlue, - backgroundColor: AppColors.primary2, - textColor: AppColors.textBlue, - ), - ], - ), - ), - + const _SearchBox(), + const _ActionButtons(), Expanded( child: Consumer( - builder: (context, viewModel, child) { - if (viewModel.isLoading) { - return const Expanded( - child: Center( - child: CircularProgressIndicator( - color: AppColors.jellyBean, - ), - ), + builder: (context, vm, child) { + if (vm.isLoading) { + return const Center( + child: CircularProgressIndicator(color: AppColors.jellyBean), ); } - if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); + if (vm.errorMessage != null) { + return Center(child: Text(vm.errorMessage!)); } - if ((viewModel.tendersResponse?.data?.tenders == null || - viewModel.tendersResponse!.data!.tenders!.isEmpty) && - viewModel.isLoading == false) { + final tenders = vm.tendersResponse?.data?.tenders ?? []; + if (tenders.isEmpty) { return const Center(child: Text(CommonStrings.noData)); } - return SingleChildScrollView( - child: SizedBox( - width: 740, - height: 800, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 4.0.h()), - child: MainTendersSlider( - tenders: viewModel.tendersResponse?.data?.tenders ?? [], - isDesktop: true, + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: SizedBox( + width: 740, + height: 800, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 4.0.h()), + child: MainTendersSlider( + tenders: tenders, + isDesktop: true, + ), + ), + ), ), ), - ), + _DesktopPagination( + totalPages: vm.totalPages, + currentPage: vm.currentPage, + onPageSelected: vm.jumpToPage, + ), + ], ); }, ), @@ -173,3 +104,174 @@ class _DesktopTendersPageState extends State { ); } } + +class _SearchBox extends StatelessWidget { + const _SearchBox(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 48.0.h(), + width: 680, + child: TextField( + style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey0), + decoration: InputDecoration( + hintText: 'Search', + hintStyle: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60), + prefixIcon: Padding( + padding: EdgeInsets.all(12.0.w()), + child: SvgPicture.asset( + AssetsManager.normalSearch, + width: 24.0.w(), + height: 24.0.h(), + ), + ), + filled: true, + fillColor: AppColors.grey0, + contentPadding: EdgeInsets.symmetric( + horizontal: 12.0.w(), + vertical: 12.0.h(), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.0.w()), + borderSide: const BorderSide(color: AppColors.mainBlue, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.0.w()), + borderSide: BorderSide(color: AppColors.grey40, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.0.w()), + borderSide: const BorderSide(color: AppColors.grey, width: 1.5), + ), + ), + ), + ); + } +} + +class _ActionButtons extends StatelessWidget { + const _ActionButtons(); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 25.0.h()), + height: 40.0.h(), + width: 680, + child: Row( + children: [ + BaseButton( + width: 178.0.w(), + isEnabled: true, + onPressed: () {}, + text: 'Filter', + icon: AssetsManager.filter, + iconColor: AppColors.textBlue, + backgroundColor: AppColors.primary2, + textColor: AppColors.textBlue, + ), + SizedBox(width: 10.0.w()), + BaseButton( + width: 178.0.w(), + isEnabled: true, + onPressed: () {}, + text: 'Sort', + icon: AssetsManager.sort, + iconColor: AppColors.textBlue, + backgroundColor: AppColors.primary2, + textColor: AppColors.textBlue, + ), + ], + ), + ); + } +} + +class _DesktopPagination extends StatelessWidget { + final int totalPages; + final int currentPage; + final ValueChanged onPageSelected; + + const _DesktopPagination({ + required this.totalPages, + required this.currentPage, + required this.onPageSelected, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 680, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.0.h()), + child: Row( + children: [ + const Spacer(), + Text( + TendersStrings.page, + style: TextStyle( + fontSize: 13.0.sp(), + fontWeight: FontWeight.w300, + color: AppColors.grey60, + ), + ), + SizedBox(width: 10.0.w()), + InkWell( + onTap: () async { + final selectedPage = await showDialog( + context: context, + builder: (context) => + PageSelectionDialog(totalPages: totalPages), + ); + if (selectedPage != null) { + onPageSelected(selectedPage); + } + }, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: AppColors.grey30, width: 1), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + SizedBox(width: 5.0.w()), + Text( + '$currentPage', + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.grey70, + ), + ), + SizedBox(width: 5.0.w()), + SvgPicture.asset(AssetsManager.arrowDownSmall), + SizedBox(width: 5.0.w()), + ], + ), + ), + ), + SizedBox(width: 5.0.w()), + Text( + TendersStrings.of, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + SizedBox(width: 5.0.w()), + Text( + '$totalPages', + style: TextStyle( + fontSize: 13.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/tenders/strings/tenders_strings.dart b/lib/views/tenders/strings/tenders_strings.dart index b4cced5..259d4a8 100644 --- a/lib/views/tenders/strings/tenders_strings.dart +++ b/lib/views/tenders/strings/tenders_strings.dart @@ -10,4 +10,6 @@ class TendersStrings { static const String tenderDeadlineLabel = 'Deadline'; static const String contracts = 'Contracts'; static const String notifications = 'Notifications'; + static const String of= 'Of'; + static const String page = 'Page'; } diff --git a/lib/views/tenders/widgets/main_tenders_slider.dart b/lib/views/tenders/widgets/main_tenders_slider.dart index 8170668..294c709 100644 --- a/lib/views/tenders/widgets/main_tenders_slider.dart +++ b/lib/views/tenders/widgets/main_tenders_slider.dart @@ -27,6 +27,31 @@ class _MainTendersSliderState extends State { final PageController pageController = PageController(initialPage: 0); int currentPage = 1; + ScrollController? _scrollController; + + @override + void initState() { + super.initState(); + + if (widget.isDesktop) { + _scrollController = ScrollController(); + _scrollController!.addListener(_onScroll); + } + } + + void _onScroll() { + if (!widget.isDesktop) { + return; + } + final viewModel = context.read(); + if (_scrollController!.position.pixels >= + _scrollController!.position.maxScrollExtent - 200 && + viewModel.hasMoreData && + !viewModel.isLoadingMore) { + viewModel.loadMoreTenders(); + } + } + void _goToPreviousPage() { if (currentPage == 1) { return; @@ -88,6 +113,7 @@ class _MainTendersSliderState extends State { @override void dispose() { pageController.dispose(); + _scrollController?.dispose(); super.dispose(); } @@ -98,19 +124,6 @@ class _MainTendersSliderState extends State { return widget.isDesktop ? _tendersListView() : _tendersSlider(isLtr); } - Widget _tendersListView() { - return ListView.separated( - padding: EdgeInsets.all(16.0.h()), - itemCount: widget.tenders.length, - separatorBuilder: - (_, __) => SizedBox(height: widget.isDesktop ? 0 : 16.0.h()), - itemBuilder: (context, index) { - final tender = widget.tenders[index]; - return TenderCard(tender: tender, isDesktop: widget.isDesktop); - }, - ); - } - Widget _tendersSlider(bool isLtr) { return Stack( children: [_tendersPageView(), _nextPreviousArrows(isLtr: isLtr)], @@ -251,4 +264,38 @@ class _MainTendersSliderState extends State { ) : SizedBox(width: 32.0.h(), height: 32.0.h()); } + + Widget _tendersListView() { + final viewModel = context.watch(); + return Column( + children: [ + Expanded( + child: ListView.separated( + padding: EdgeInsets.all(16.0.h()), + itemCount: + widget.tenders.length + + (widget.isDesktop && viewModel.isLoadingMore ? 1 : 0), + separatorBuilder: + (_, __) => SizedBox(height: widget.isDesktop ? 0 : 16.0.h()), + itemBuilder: (context, index) { + if (index < widget.tenders.length) { + final tender = widget.tenders[index]; + return TenderCard(tender: tender, isDesktop: widget.isDesktop); + } else { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppColors.jellyBean, + ), + ), + ); + } + }, + ), + ), + ], + ); + } } diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js index 4630252..730ae3d 100644 --- a/web/firebase-messaging-sw.js +++ b/web/firebase-messaging-sw.js @@ -2,13 +2,13 @@ importScripts("https://www.gstatic.com/firebasejs/9.22.2/firebase-app-compat.js" importScripts("https://www.gstatic.com/firebasejs/9.22.2/firebase-messaging-compat.js"); firebase.initializeApp({ - apiKey: 'AIzaSyAUQhM789-0qZIaIWtwTQXyz9gEtN-TnnU', - authDomain: 'opplens.firebaseapp.com', - appId: '1:1071558853812:web:d9ca896b088ee7da48c3e1', - messagingSenderId: '1071558853812', - projectId: 'opplens', - storageBucket: 'opplens.firebasestorage.app', - measurementId: 'G-GJGP9WB28B', + apiKey: "AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0", + authDomain: "opplens-270d1.firebaseapp.com", + projectId: "opplens-270d1", + storageBucket: "opplens-270d1.firebasestorage.app", + messagingSenderId: "6923326255", + appId: "1:6923326255:web:c71763ece06aaaf3ed87da", + measurementId: "G-M5HQR4YZVS" }); const messaging = firebase.messaging();