From 826e6b2b55dd7a7f8eb2ed4a2586984e8eb73367 Mon Sep 17 00:00:00 2001 From: Hajar ELHILALI Date: Thu, 28 May 2026 17:48:54 +0200 Subject: [PATCH 1/5] opponent profile : header section & app bar --- .../deepdame/lib/core/routing/app_router.dart | 9 + .../providers/sent_requests_provider.dart | 15 + .../providers/sent_requests_provider.g.dart | 63 ++++ .../widgets/add_friend_sheet.dart | 57 +++- .../presentation/widgets/friend_tile.dart | 8 +- .../data/models/public_user_profile.dart | 17 ++ .../models/public_user_profile.freezed.dart | 286 ++++++++++++++++++ .../data/models/public_user_profile.g.dart | 23 ++ .../data/repositories/user_repository.dart | 24 ++ .../data/repositories/user_repository.g.dart | 51 ++++ .../opponent_profile_screen.dart | 66 ++++ .../providers/opponent_profile_provider.dart | 10 + .../opponent_profile_provider.g.dart | 87 ++++++ .../widgets/opponent_app_bar.dart | 111 +++++++ .../widgets/opponent_header_section.dart | 145 +++++++++ 15 files changed, 963 insertions(+), 9 deletions(-) create mode 100644 client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.dart create mode 100644 client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.g.dart create mode 100644 client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.dart create mode 100644 client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.freezed.dart create mode 100644 client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.g.dart create mode 100644 client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.dart create mode 100644 client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.g.dart create mode 100644 client/deepdame/lib/features/opponent_profile/opponent_profile_screen.dart create mode 100644 client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.dart create mode 100644 client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.g.dart create mode 100644 client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart create mode 100644 client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart diff --git a/client/deepdame/lib/core/routing/app_router.dart b/client/deepdame/lib/core/routing/app_router.dart index 94964c7..ea7e784 100644 --- a/client/deepdame/lib/core/routing/app_router.dart +++ b/client/deepdame/lib/core/routing/app_router.dart @@ -18,6 +18,7 @@ import 'package:deepdame/features/home/data/models/open_game.dart'; import 'package:deepdame/features/home/home_screen.dart'; import 'package:deepdame/features/home/lobby_screen.dart'; import 'package:deepdame/features/onboarding/onboarding_screen.dart'; +import 'package:deepdame/features/opponent_profile/opponent_profile_screen.dart'; import 'package:deepdame/features/profile/profile_screen.dart'; import 'package:deepdame/features/settings/settings_screen.dart'; import 'package:deepdame/features/settings/theme_picker_screen.dart'; @@ -128,6 +129,14 @@ final appRouter = GoRouter( path: '/user', builder: (context, state) => const UserScreen(), ), + GoRoute( + name: 'opponent-profile', + path: '/opponent-profile/:userId', + builder: (context, state) { + final userId = state.pathParameters['userId']!; + return OpponentProfileScreen(userId: userId); + }, + ), GoRoute( name: 'settings', path: '/settings', diff --git a/client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.dart b/client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.dart new file mode 100644 index 0000000..a85d4b3 --- /dev/null +++ b/client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.dart @@ -0,0 +1,15 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'sent_requests_provider.g.dart'; + +@Riverpod(keepAlive: true) +class SentFriendRequests extends _$SentFriendRequests { + @override + Set build() => {}; + + void add(String userId) { + state = {...state, userId}; + } + + bool contains(String userId) => state.contains(userId); +} diff --git a/client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.g.dart b/client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.g.dart new file mode 100644 index 0000000..21ca776 --- /dev/null +++ b/client/deepdame/lib/features/friends/presentation/providers/sent_requests_provider.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sent_requests_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SentFriendRequests) +final sentFriendRequestsProvider = SentFriendRequestsProvider._(); + +final class SentFriendRequestsProvider + extends $NotifierProvider> { + SentFriendRequestsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'sentFriendRequestsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$sentFriendRequestsHash(); + + @$internal + @override + SentFriendRequests create() => SentFriendRequests(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Set value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$sentFriendRequestsHash() => + r'db94b3fa5d1524075d8026acf6f03fe8b93f8aa9'; + +abstract class _$SentFriendRequests extends $Notifier> { + Set build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, Set>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, Set>, + Set, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart b/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart index 676521c..14a7f5f 100644 --- a/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart +++ b/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart @@ -4,9 +4,11 @@ import 'package:deepdame/core/network/stomp_service.dart'; import 'package:deepdame/features/friends/data/models/user_search_result.dart'; import 'package:deepdame/features/friends/data/repositories/friends_repository.dart'; import 'package:deepdame/features/friends/presentation/providers/friends_provider.dart'; +import 'package:deepdame/features/friends/presentation/providers/sent_requests_provider.dart'; import 'package:deepdame/shared/profile_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; class AddFriendSheet extends ConsumerStatefulWidget { const AddFriendSheet({super.key}); @@ -70,7 +72,15 @@ class _AddFriendSheetState extends ConsumerState { } void _sendRequest(UserSearchResult result) { - ref.read(stompServiceProvider).send('/app/invite/friend/${result.id}'); + final stomp = ref.read(stompServiceProvider); + if (!stomp.isConnected) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Connection lost. Try again.')), + ); + return; + } + stomp.send('/app/invite/friend/${result.id}'); + ref.read(sentFriendRequestsProvider.notifier).add(result.id); setState(() { _results = _results .map( @@ -252,16 +262,29 @@ class _AddFriendSheetState extends ConsumerState { } if (_results.isNotEmpty) { + final sentIds = ref.watch(sentFriendRequestsProvider); return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 300), child: ListView.builder( shrinkWrap: true, itemCount: _results.length, - itemBuilder: (_, i) => _SearchResultTile( - result: _results[i], - onAdd: () => _sendRequest(_results[i]), - onAccept: () => _acceptRequest(_results[i]), - ), + itemBuilder: (_, i) { + final result = _results[i]; + final effectiveRelationship = + result.relationship == RelationshipStatus.none && + sentIds.contains(result.id) + ? RelationshipStatus.requestSent + : result.relationship; + return _SearchResultTile( + result: result.copyWith(relationship: effectiveRelationship), + onAdd: () => _sendRequest(result), + onAccept: () => _acceptRequest(result), + onView: () => context.pushNamed( + 'opponent-profile', + pathParameters: {'userId': result.id}, + ), + ); + }, ), ); } @@ -296,11 +319,13 @@ class _SearchResultTile extends StatelessWidget { required this.result, required this.onAdd, required this.onAccept, + required this.onView, }); final UserSearchResult result; final VoidCallback onAdd; final VoidCallback onAccept; + final VoidCallback onView; @override Widget build(BuildContext context) { @@ -336,6 +361,26 @@ class _SearchResultTile extends StatelessWidget { ), ), ), + OutlinedButton( + onPressed: onView, + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.onSurface.withValues(alpha: 0.7), + side: BorderSide( + color: colorScheme.onSurface.withValues(alpha: 0.25), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + textStyle: textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + child: const Text('View'), + ), + const SizedBox(width: 6), _RelationshipBadge( relationship: result.relationship, onAdd: onAdd, diff --git a/client/deepdame/lib/features/friends/presentation/widgets/friend_tile.dart b/client/deepdame/lib/features/friends/presentation/widgets/friend_tile.dart index 6e1c77e..9f1468a 100644 --- a/client/deepdame/lib/features/friends/presentation/widgets/friend_tile.dart +++ b/client/deepdame/lib/features/friends/presentation/widgets/friend_tile.dart @@ -5,6 +5,7 @@ import 'package:deepdame/features/friends/presentation/providers/friends_provide import 'package:deepdame/shared/profile_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; class FriendTile extends ConsumerStatefulWidget { const FriendTile({ @@ -131,9 +132,10 @@ class _FriendTileState extends ConsumerState { } } - void _onView() { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Profile coming soon')), + Future _onView() async { + await context.pushNamed( + 'opponent-profile', + pathParameters: {'userId': widget.friend.id}, ); } diff --git a/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.dart b/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.dart new file mode 100644 index 0000000..33376de --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'public_user_profile.freezed.dart'; +part 'public_user_profile.g.dart'; + +@freezed +abstract class PublicUserProfile with _$PublicUserProfile { + const factory PublicUserProfile({ + required String id, + required String username, + required bool online, + String? imageUrl, + }) = _PublicUserProfile; + + factory PublicUserProfile.fromJson(Map json) => + _$PublicUserProfileFromJson(json); +} diff --git a/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.freezed.dart b/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.freezed.dart new file mode 100644 index 0000000..190e163 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.freezed.dart @@ -0,0 +1,286 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// 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 'public_user_profile.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$PublicUserProfile { + + String get id; String get username; bool get online; String? get imageUrl; +/// Create a copy of PublicUserProfile +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PublicUserProfileCopyWith get copyWith => _$PublicUserProfileCopyWithImpl(this as PublicUserProfile, _$identity); + + /// Serializes this PublicUserProfile to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PublicUserProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.username, username) || other.username == username)&&(identical(other.online, online) || other.online == online)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,username,online,imageUrl); + +@override +String toString() { + return 'PublicUserProfile(id: $id, username: $username, online: $online, imageUrl: $imageUrl)'; +} + + +} + +/// @nodoc +abstract mixin class $PublicUserProfileCopyWith<$Res> { + factory $PublicUserProfileCopyWith(PublicUserProfile value, $Res Function(PublicUserProfile) _then) = _$PublicUserProfileCopyWithImpl; +@useResult +$Res call({ + String id, String username, bool online, String? imageUrl +}); + + + + +} +/// @nodoc +class _$PublicUserProfileCopyWithImpl<$Res> + implements $PublicUserProfileCopyWith<$Res> { + _$PublicUserProfileCopyWithImpl(this._self, this._then); + + final PublicUserProfile _self; + final $Res Function(PublicUserProfile) _then; + +/// Create a copy of PublicUserProfile +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? username = null,Object? online = null,Object? imageUrl = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,username: null == username ? _self.username : username // ignore: cast_nullable_to_non_nullable +as String,online: null == online ? _self.online : online // ignore: cast_nullable_to_non_nullable +as bool,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [PublicUserProfile]. +extension PublicUserProfilePatterns on PublicUserProfile { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _PublicUserProfile value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _PublicUserProfile() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _PublicUserProfile value) $default,){ +final _that = this; +switch (_that) { +case _PublicUserProfile(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _PublicUserProfile value)? $default,){ +final _that = this; +switch (_that) { +case _PublicUserProfile() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String username, bool online, String? imageUrl)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _PublicUserProfile() when $default != null: +return $default(_that.id,_that.username,_that.online,_that.imageUrl);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String username, bool online, String? imageUrl) $default,) {final _that = this; +switch (_that) { +case _PublicUserProfile(): +return $default(_that.id,_that.username,_that.online,_that.imageUrl);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String username, bool online, String? imageUrl)? $default,) {final _that = this; +switch (_that) { +case _PublicUserProfile() when $default != null: +return $default(_that.id,_that.username,_that.online,_that.imageUrl);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _PublicUserProfile implements PublicUserProfile { + const _PublicUserProfile({required this.id, required this.username, required this.online, this.imageUrl}); + factory _PublicUserProfile.fromJson(Map json) => _$PublicUserProfileFromJson(json); + +@override final String id; +@override final String username; +@override final bool online; +@override final String? imageUrl; + +/// Create a copy of PublicUserProfile +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PublicUserProfileCopyWith<_PublicUserProfile> get copyWith => __$PublicUserProfileCopyWithImpl<_PublicUserProfile>(this, _$identity); + +@override +Map toJson() { + return _$PublicUserProfileToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PublicUserProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.username, username) || other.username == username)&&(identical(other.online, online) || other.online == online)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,username,online,imageUrl); + +@override +String toString() { + return 'PublicUserProfile(id: $id, username: $username, online: $online, imageUrl: $imageUrl)'; +} + + +} + +/// @nodoc +abstract mixin class _$PublicUserProfileCopyWith<$Res> implements $PublicUserProfileCopyWith<$Res> { + factory _$PublicUserProfileCopyWith(_PublicUserProfile value, $Res Function(_PublicUserProfile) _then) = __$PublicUserProfileCopyWithImpl; +@override @useResult +$Res call({ + String id, String username, bool online, String? imageUrl +}); + + + + +} +/// @nodoc +class __$PublicUserProfileCopyWithImpl<$Res> + implements _$PublicUserProfileCopyWith<$Res> { + __$PublicUserProfileCopyWithImpl(this._self, this._then); + + final _PublicUserProfile _self; + final $Res Function(_PublicUserProfile) _then; + +/// Create a copy of PublicUserProfile +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? username = null,Object? online = null,Object? imageUrl = freezed,}) { + return _then(_PublicUserProfile( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,username: null == username ? _self.username : username // ignore: cast_nullable_to_non_nullable +as String,online: null == online ? _self.online : online // ignore: cast_nullable_to_non_nullable +as bool,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.g.dart b/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.g.dart new file mode 100644 index 0000000..3871889 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/data/models/public_user_profile.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'public_user_profile.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_PublicUserProfile _$PublicUserProfileFromJson(Map json) => + _PublicUserProfile( + id: json['id'] as String, + username: json['username'] as String, + online: json['online'] as bool, + imageUrl: json['imageUrl'] as String?, + ); + +Map _$PublicUserProfileToJson(_PublicUserProfile instance) => + { + 'id': instance.id, + 'username': instance.username, + 'online': instance.online, + 'imageUrl': instance.imageUrl, + }; diff --git a/client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.dart b/client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.dart new file mode 100644 index 0000000..0c22c46 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.dart @@ -0,0 +1,24 @@ +import 'package:deepdame/core/network/dio_client.dart'; +import 'package:deepdame/features/opponent_profile/data/models/public_user_profile.dart'; +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'user_repository.g.dart'; + +@riverpod +UserRepository userRepository(Ref ref) { + return UserRepository(ref.watch(dioProvider)); +} + +class UserRepository { + UserRepository(this._dio); + + final Dio _dio; + + Future getUser(String userId) async { + final response = await _dio.get>( + '/api/v1/user/$userId', + ); + return PublicUserProfile.fromJson(response.data!); + } +} diff --git a/client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.g.dart b/client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.g.dart new file mode 100644 index 0000000..4822266 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/data/repositories/user_repository.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(userRepository) +final userRepositoryProvider = UserRepositoryProvider._(); + +final class UserRepositoryProvider + extends $FunctionalProvider + with $Provider { + UserRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + UserRepository create(Ref ref) { + return userRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(UserRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$userRepositoryHash() => r'08857b9b2d45b6cea0baab7b8549844ff84a1f4a'; diff --git a/client/deepdame/lib/features/opponent_profile/opponent_profile_screen.dart b/client/deepdame/lib/features/opponent_profile/opponent_profile_screen.dart new file mode 100644 index 0000000..5df81ce --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/opponent_profile_screen.dart @@ -0,0 +1,66 @@ +import 'package:deepdame/features/opponent_profile/presentation/providers/opponent_profile_provider.dart'; +import 'package:deepdame/features/opponent_profile/presentation/widgets/opponent_app_bar.dart'; +import 'package:deepdame/features/opponent_profile/presentation/widgets/opponent_header_section.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class OpponentProfileScreen extends ConsumerWidget { + const OpponentProfileScreen({required this.userId, super.key}); + + final String userId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final profileAsync = ref.watch(opponentProfileProvider(userId)); + + return Scaffold( + appBar: OpponentAppBar(userId: userId), + body: profileAsync.when( + loading: () => const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + SizedBox(height: 40), + Center(child: OpponentHeaderSkeleton()), + ], + ), + ), + error: (_, _) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Failed to load profile', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.45), + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () => ref.invalidate( + opponentProfileProvider(userId), + ), + child: const Text('Retry'), + ), + ], + ), + ), + data: (profile) => SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Center(child: OpponentHeaderSection(profile: profile)), + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ); + } +} diff --git a/client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.dart b/client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.dart new file mode 100644 index 0000000..88613ad --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.dart @@ -0,0 +1,10 @@ +import 'package:deepdame/features/opponent_profile/data/models/public_user_profile.dart'; +import 'package:deepdame/features/opponent_profile/data/repositories/user_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'opponent_profile_provider.g.dart'; + +@riverpod +Future opponentProfile(Ref ref, String userId) { + return ref.read(userRepositoryProvider).getUser(userId); +} diff --git a/client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.g.dart b/client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.g.dart new file mode 100644 index 0000000..e08d973 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/presentation/providers/opponent_profile_provider.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'opponent_profile_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(opponentProfile) +final opponentProfileProvider = OpponentProfileFamily._(); + +final class OpponentProfileProvider + extends + $FunctionalProvider< + AsyncValue, + PublicUserProfile, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + OpponentProfileProvider._({ + required OpponentProfileFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'opponentProfileProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$opponentProfileHash(); + + @override + String toString() { + return r'opponentProfileProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return opponentProfile(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is OpponentProfileProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$opponentProfileHash() => r'bc0ae9bf6645a64c7389501e7031ad29ca3279d4'; + +final class OpponentProfileFamily extends $Family + with $FunctionalFamilyOverride, String> { + OpponentProfileFamily._() + : super( + retry: null, + name: r'opponentProfileProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + OpponentProfileProvider call(String userId) => + OpponentProfileProvider._(argument: userId, from: this); + + @override + String toString() => r'opponentProfileProvider'; +} diff --git a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart new file mode 100644 index 0000000..b7f66c5 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart @@ -0,0 +1,111 @@ +import 'package:deepdame/core/network/stomp_service.dart'; +import 'package:deepdame/features/friends/presentation/providers/friends_provider.dart'; +import 'package:deepdame/features/friends/presentation/providers/sent_requests_provider.dart'; +import 'package:deepdame/features/opponent_profile/presentation/providers/opponent_profile_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { + const OpponentAppBar({required this.userId, super.key}); + + final String userId; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + void _sendFriendRequest(BuildContext context, WidgetRef ref) { + final stomp = ref.read(stompServiceProvider); + if (!stomp.isConnected) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Connection lost. Try again.')), + ); + return; + } + stomp.send('/app/invite/friend/$userId'); + ref.read(sentFriendRequestsProvider.notifier).add(userId); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final profileAsync = ref.watch(opponentProfileProvider(userId)); + final isFriend = ref + .watch(friendsProvider) + .asData + ?.value + .any((f) => f.id == userId) ?? + false; + final requestSent = ref + .watch(sentFriendRequestsProvider) + .contains(userId); + + return AppBar( + leading: const BackButton(), + centerTitle: false, + title: profileAsync.when( + loading: () => const SizedBox.shrink(), + error: (_, _) => const SizedBox.shrink(), + data: (p) => Text( + p.username, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + ), + actions: [ + if (!isFriend && !requestSent) + OutlinedButton( + onPressed: () => _sendFriendRequest(context, ref), + style: OutlinedButton.styleFrom( + side: BorderSide(color: colorScheme.primary), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + '+ Friend', + style: textTheme.labelSmall?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + if (!isFriend && requestSent) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'Sent', + style: textTheme.labelSmall?.copyWith( + color: colorScheme.onSurface.withValues(alpha: 0.45), + fontWeight: FontWeight.w500, + ), + ), + ), + if (!isFriend) const SizedBox(width: 8), + FilledButton( + onPressed: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Game invite coming soon')), + ), + style: FilledButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + 'Invite', + style: textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), + ), + ), + const SizedBox(width: 16), + ], + ); + } +} diff --git a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart new file mode 100644 index 0000000..628d089 --- /dev/null +++ b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart @@ -0,0 +1,145 @@ +import 'package:deepdame/core/theme/themes/extensions/status_pill_colors.dart'; +import 'package:deepdame/features/home/presentation/widgets/skeleton_wrapper.dart'; +import 'package:deepdame/features/opponent_profile/data/models/public_user_profile.dart'; +import 'package:deepdame/shared/profile_icon.dart'; +import 'package:flutter/material.dart'; + +class OpponentHeaderSection extends StatelessWidget { + const OpponentHeaderSection({required this.profile, super.key}); + + final PublicUserProfile profile; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Column( + children: [ + _AvatarWithOnlineDot( + username: profile.username, + imageUrl: profile.imageUrl, + online: profile.online, + ), + const SizedBox(height: 12), + Text( + profile.username, + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 6), + _OnlineBadge(online: profile.online), + ], + ); + } +} + +class OpponentHeaderSkeleton extends StatelessWidget { + const OpponentHeaderSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + final ph = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.08); + + return SkeletonWrapper( + child: Column( + children: [ + Container( + width: 90, + height: 90, + decoration: BoxDecoration(color: ph, shape: BoxShape.circle), + ), + const SizedBox(height: 12), + Container( + width: 110, + height: 18, + decoration: BoxDecoration( + color: ph, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(height: 8), + Container( + width: 64, + height: 24, + decoration: BoxDecoration( + color: ph, + borderRadius: BorderRadius.circular(20), + ), + ), + ], + ), + ); + } +} + +class _AvatarWithOnlineDot extends StatelessWidget { + const _AvatarWithOnlineDot({ + required this.username, + required this.online, + this.imageUrl, + }); + + final String username; + final String? imageUrl; + final bool online; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Stack( + clipBehavior: Clip.none, + children: [ + ProfileIcon.large(username: username, imageUrl: imageUrl), + if (online) + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Theme.of( + context, + ).extension()!.onlineText, + shape: BoxShape.circle, + border: Border.all(color: colorScheme.surface, width: 2.5), + ), + ), + ), + ], + ); + } +} + +class _OnlineBadge extends StatelessWidget { + const _OnlineBadge({required this.online}); + + final bool online; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final pillColors = Theme.of(context).extension()!; + final colors = online ? pillColors.online : pillColors.offline; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + border: Border.all(color: colors.border), + borderRadius: BorderRadius.circular(20), + color: colors.background, + ), + child: Text( + online ? 'Online' : 'Offline', + style: textTheme.labelSmall?.copyWith( + color: colors.text, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} From ad8cf56a3f08a4eda4474850489fe50cd6aecf72 Mon Sep 17 00:00:00 2001 From: Hajar ELHILALI Date: Thu, 28 May 2026 18:16:45 +0200 Subject: [PATCH 2/5] fixer le design --- .../widgets/add_friend_sheet.dart | 11 ++++--- .../widgets/opponent_app_bar.dart | 29 ++++------------- .../widgets/opponent_header_section.dart | 32 ++----------------- .../widgets/profile_recent_games_section.dart | 18 ++--------- 4 files changed, 18 insertions(+), 72 deletions(-) diff --git a/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart b/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart index 14a7f5f..1e6e9d0 100644 --- a/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart +++ b/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart @@ -272,9 +272,9 @@ class _AddFriendSheetState extends ConsumerState { final result = _results[i]; final effectiveRelationship = result.relationship == RelationshipStatus.none && - sentIds.contains(result.id) - ? RelationshipStatus.requestSent - : result.relationship; + sentIds.contains(result.id) + ? RelationshipStatus.requestSent + : result.relationship; return _SearchResultTile( result: result.copyWith(relationship: effectiveRelationship), onAdd: () => _sendRequest(result), @@ -368,7 +368,10 @@ class _SearchResultTile extends StatelessWidget { side: BorderSide( color: colorScheme.onSurface.withValues(alpha: 0.25), ), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( diff --git a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart index b7f66c5..32258bd 100644 --- a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart +++ b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart @@ -2,6 +2,7 @@ import 'package:deepdame/core/network/stomp_service.dart'; import 'package:deepdame/features/friends/presentation/providers/friends_provider.dart'; import 'package:deepdame/features/friends/presentation/providers/sent_requests_provider.dart'; import 'package:deepdame/features/opponent_profile/presentation/providers/opponent_profile_provider.dart'; +import 'package:deepdame/shared/status_pill.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -30,15 +31,10 @@ class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final profileAsync = ref.watch(opponentProfileProvider(userId)); - final isFriend = ref - .watch(friendsProvider) - .asData - ?.value - .any((f) => f.id == userId) ?? + final isFriend = + ref.watch(friendsProvider).asData?.value.any((f) => f.id == userId) ?? false; - final requestSent = ref - .watch(sentFriendRequestsProvider) - .contains(userId); + final requestSent = ref.watch(sentFriendRequestsProvider).contains(userId); return AppBar( leading: const BackButton(), @@ -87,22 +83,11 @@ class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { ), ), if (!isFriend) const SizedBox(width: 8), - FilledButton( - onPressed: () => ScaffoldMessenger.of(context).showSnackBar( + GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Game invite coming soon')), ), - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - 'Invite', - style: textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), - ), + child: const StatusPill.invite(), ), const SizedBox(width: 16), ], diff --git a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart index 628d089..47fd185 100644 --- a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart +++ b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart @@ -2,6 +2,7 @@ import 'package:deepdame/core/theme/themes/extensions/status_pill_colors.dart'; import 'package:deepdame/features/home/presentation/widgets/skeleton_wrapper.dart'; import 'package:deepdame/features/opponent_profile/data/models/public_user_profile.dart'; import 'package:deepdame/shared/profile_icon.dart'; +import 'package:deepdame/shared/status_pill.dart'; import 'package:flutter/material.dart'; class OpponentHeaderSection extends StatelessWidget { @@ -30,7 +31,7 @@ class OpponentHeaderSection extends StatelessWidget { ), ), const SizedBox(height: 6), - _OnlineBadge(online: profile.online), + if (profile.online) const StatusPill.online() else const StatusPill.offline(), ], ); } @@ -114,32 +115,3 @@ class _AvatarWithOnlineDot extends StatelessWidget { ); } } - -class _OnlineBadge extends StatelessWidget { - const _OnlineBadge({required this.online}); - - final bool online; - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final pillColors = Theme.of(context).extension()!; - final colors = online ? pillColors.online : pillColors.offline; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - border: Border.all(color: colors.border), - borderRadius: BorderRadius.circular(20), - color: colors.background, - ), - child: Text( - online ? 'Online' : 'Offline', - style: textTheme.labelSmall?.copyWith( - color: colors.text, - fontWeight: FontWeight.w600, - ), - ), - ); - } -} diff --git a/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart b/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart index 46fe5b9..2b976f7 100644 --- a/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart +++ b/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart @@ -2,6 +2,7 @@ import 'package:deepdame/features/home/presentation/widgets/skeleton_wrapper.dar import 'package:deepdame/features/profile/data/models/game_history_entry.dart'; import 'package:deepdame/features/profile/presentation/providers/game_history_provider.dart'; import 'package:deepdame/shared/profile_icon.dart'; +import 'package:deepdame/shared/status_pill.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -80,8 +81,6 @@ class _GameHistoryTile extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final badgeColor = entry.won ? colorScheme.primary : colorScheme.tertiary; - return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), @@ -125,20 +124,7 @@ class _GameHistoryTile extends StatelessWidget { ), ), const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: badgeColor.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - entry.won ? 'Win' : 'Loss', - style: textTheme.labelSmall?.copyWith( - color: badgeColor, - fontWeight: FontWeight.w700, - ), - ), - ), + if (entry.won) const StatusPill.win() else const StatusPill.loss(), const SizedBox(width: 10), Text( '${entry.moveCount} moves', From 6f0e8c317b417399deaa21ef219b2d185dcb01db Mon Sep 17 00:00:00 2001 From: Hajar ELHILALI Date: Thu, 28 May 2026 18:21:14 +0200 Subject: [PATCH 3/5] format --- .../presentation/widgets/opponent_header_section.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart index 47fd185..a5dc697 100644 --- a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart +++ b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_header_section.dart @@ -31,7 +31,10 @@ class OpponentHeaderSection extends StatelessWidget { ), ), const SizedBox(height: 6), - if (profile.online) const StatusPill.online() else const StatusPill.offline(), + if (profile.online) + const StatusPill.online() + else + const StatusPill.offline(), ], ); } From 90dff6c3f849311478a0b2761a31690867a45312 Mon Sep 17 00:00:00 2001 From: Hajar ELHILALI Date: Thu, 28 May 2026 18:31:32 +0200 Subject: [PATCH 4/5] fixer le cas ou friend request deja envoye et afficher accept dans opponent profile --- .../widgets/opponent_app_bar.dart | 132 +++++++++++++----- 1 file changed, 96 insertions(+), 36 deletions(-) diff --git a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart index 32258bd..fea6471 100644 --- a/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart +++ b/client/deepdame/lib/features/opponent_profile/presentation/widgets/opponent_app_bar.dart @@ -1,4 +1,5 @@ import 'package:deepdame/core/network/stomp_service.dart'; +import 'package:deepdame/features/friends/data/repositories/friends_repository.dart'; import 'package:deepdame/features/friends/presentation/providers/friends_provider.dart'; import 'package:deepdame/features/friends/presentation/providers/sent_requests_provider.dart'; import 'package:deepdame/features/opponent_profile/presentation/providers/opponent_profile_provider.dart'; @@ -6,7 +7,8 @@ import 'package:deepdame/shared/status_pill.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { +class OpponentAppBar extends ConsumerStatefulWidget + implements PreferredSizeWidget { const OpponentAppBar({required this.userId, super.key}); final String userId; @@ -14,7 +16,12 @@ class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); - void _sendFriendRequest(BuildContext context, WidgetRef ref) { + @override + ConsumerState createState() => _OpponentAppBarState(); +} + +class _OpponentAppBarState extends ConsumerState { + void _sendFriendRequest() { final stomp = ref.read(stompServiceProvider); if (!stomp.isConnected) { ScaffoldMessenger.of(context).showSnackBar( @@ -22,19 +29,48 @@ class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { ); return; } - stomp.send('/app/invite/friend/$userId'); - ref.read(sentFriendRequestsProvider.notifier).add(userId); + stomp.send('/app/invite/friend/${widget.userId}'); + ref.read(sentFriendRequestsProvider.notifier).add(widget.userId); + } + + Future _acceptRequest(String requestId) async { + try { + await ref.read(friendsRepositoryProvider).acceptRequest(requestId); + ref + ..invalidate(friendRequestsProvider) + ..invalidate(friendsProvider); + } on Exception { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to accept request. Try again.'), + ), + ); + } + } } @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final profileAsync = ref.watch(opponentProfileProvider(userId)); + final profileAsync = ref.watch(opponentProfileProvider(widget.userId)); final isFriend = - ref.watch(friendsProvider).asData?.value.any((f) => f.id == userId) ?? + ref + .watch(friendsProvider) + .asData + ?.value + .any((f) => f.id == widget.userId) ?? false; - final requestSent = ref.watch(sentFriendRequestsProvider).contains(userId); + final requestSent = ref + .watch(sentFriendRequestsProvider) + .contains(widget.userId); + final pendingRequest = ref + .watch(friendRequestsProvider) + .asData + ?.value + .where((r) => r.sender.id == widget.userId) + .firstOrNull; return AppBar( leading: const BackButton(), @@ -51,38 +87,62 @@ class OpponentAppBar extends ConsumerWidget implements PreferredSizeWidget { ), ), actions: [ - if (!isFriend && !requestSent) - OutlinedButton( - onPressed: () => _sendFriendRequest(context, ref), - style: OutlinedButton.styleFrom( - side: BorderSide(color: colorScheme.primary), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + if (!isFriend) ...[ + if (pendingRequest != null) + TextButton( + onPressed: () => _acceptRequest(pendingRequest.id), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - '+ Friend', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, + child: Text( + 'Accept', + style: textTheme.labelSmall?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w700, + ), ), - ), - ), - if (!isFriend && requestSent) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'Sent', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurface.withValues(alpha: 0.45), - fontWeight: FontWeight.w500, + ) + else if (requestSent) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'Sent', + style: textTheme.labelSmall?.copyWith( + color: colorScheme.onSurface.withValues(alpha: 0.45), + fontWeight: FontWeight.w500, + ), + ), + ) + else + OutlinedButton( + onPressed: _sendFriendRequest, + style: OutlinedButton.styleFrom( + side: BorderSide(color: colorScheme.primary), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + '+ Friend', + style: textTheme.labelSmall?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), ), ), - ), - if (!isFriend) const SizedBox(width: 8), + const SizedBox(width: 8), + ], GestureDetector( onTap: () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Game invite coming soon')), From 1f6dd5f46fca2dc4072a71caad1887752cb5fa8f Mon Sep 17 00:00:00 2001 From: Hajar ELHILALI Date: Thu, 28 May 2026 19:30:50 +0200 Subject: [PATCH 5/5] fixer le code --- .../friends/presentation/widgets/add_friend_sheet.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart b/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart index 1e6e9d0..cc4546e 100644 --- a/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart +++ b/client/deepdame/lib/features/friends/presentation/widgets/add_friend_sheet.dart @@ -263,6 +263,9 @@ class _AddFriendSheetState extends ConsumerState { if (_results.isNotEmpty) { final sentIds = ref.watch(sentFriendRequestsProvider); + final friendIds = + ref.watch(friendsProvider).asData?.value.map((f) => f.id).toSet() ?? + {}; return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 300), child: ListView.builder( @@ -270,9 +273,10 @@ class _AddFriendSheetState extends ConsumerState { itemCount: _results.length, itemBuilder: (_, i) { final result = _results[i]; - final effectiveRelationship = - result.relationship == RelationshipStatus.none && - sentIds.contains(result.id) + final effectiveRelationship = friendIds.contains(result.id) + ? RelationshipStatus.friends + : result.relationship == RelationshipStatus.none && + sentIds.contains(result.id) ? RelationshipStatus.requestSent : result.relationship; return _SearchResultTile(