diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b005a9f..7874a1b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -112,5 +112,6 @@ "eventsCount": "Events", "followersCount": "Followers", "followingCount": "Following", - "yourEvents": "Your events" + "yourEvents": "Your events", + "failedToLoadUserProfile": "Failed to load user's profile" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index c0424f8..71f0e4c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -112,5 +112,6 @@ "eventsCount": "Wydarzenia", "followersCount": "Obserwujący", "followingCount": "Obserwowani", - "yourEvents": "Twoje wydarzenia" -} + "yourEvents": "Twoje wydarzenia", + "failedToLoadUserProfile": "Nie udało się załadować profilu użytkownika" +} \ No newline at end of file diff --git a/lib/src/core/presentation/app_initializer.dart b/lib/src/core/presentation/app_initializer.dart index cede5c0..099d0fe 100644 --- a/lib/src/core/presentation/app_initializer.dart +++ b/lib/src/core/presentation/app_initializer.dart @@ -11,6 +11,7 @@ import 'package:interns2025b_mobile/src/features/auth/presentation/pages/registe import 'package:interns2025b_mobile/src/features/event/presentation/pages/event_creation_page.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/pages/event_details_page.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/pages/event_page.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/pages/profile_details_page.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/pages/profile_page.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_user_provider.dart'; import 'package:interns2025b_mobile/src/shared/presentation/providers/localization_controller_provider.dart'; @@ -44,6 +45,7 @@ class AppInitializer extends ConsumerWidget { AppRoutes.addEvent: (context) => EventCreationPage(), AppRoutes.events: (context) => EventPage(), AppRoutes.eventDetails: (context) => EventDetails(), + AppRoutes.profileDetails: (context) => ProfileDetailsPage(), }, ); } diff --git a/lib/src/core/routes/app_routes.dart b/lib/src/core/routes/app_routes.dart index ea59ba0..a89a252 100644 --- a/lib/src/core/routes/app_routes.dart +++ b/lib/src/core/routes/app_routes.dart @@ -6,4 +6,5 @@ class AppRoutes { static const String profile = '/profile'; static const String addEvent = '/add-event'; static const String eventDetails = '/event-details'; + static const String profileDetails = '/profile-details'; } diff --git a/lib/src/features/auth/presentation/controllers/auth_controller.dart b/lib/src/features/auth/presentation/controllers/auth_controller.dart index 0b4e03b..91f39a3 100644 --- a/lib/src/features/auth/presentation/controllers/auth_controller.dart +++ b/lib/src/features/auth/presentation/controllers/auth_controller.dart @@ -16,6 +16,7 @@ import 'package:interns2025b_mobile/src/features/auth/domain/usecases/forgot_pas import 'package:interns2025b_mobile/src/features/auth/domain/usecases/login_usecase.dart'; import 'package:interns2025b_mobile/src/features/auth/domain/usecases/logout_usecase.dart'; import 'package:interns2025b_mobile/src/features/auth/domain/usecases/register_usecase.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_user_provider.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -49,6 +50,7 @@ class AuthController extends AsyncNotifier { try { await _loginUseCase(email, password); final user = await _loadUser(); + ref.read(profileUserProvider.notifier).state = user; state = AsyncData(user); if (context.mounted) { @@ -112,6 +114,8 @@ class AuthController extends AsyncNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.remove('token'); await prefs.remove('user'); + ref.read(profileUserProvider.notifier).state = null; + state = const AsyncData(null); diff --git a/lib/src/features/event/presentation/widgets/event_author_tile.dart b/lib/src/features/event/presentation/widgets/event_author_tile.dart index 5cf9dc3..624d47a 100644 --- a/lib/src/features/event/presentation/widgets/event_author_tile.dart +++ b/lib/src/features/event/presentation/widgets/event_author_tile.dart @@ -1,47 +1,68 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/core/routes/app_routes.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_user_provider.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_owner.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; -class EventAuthorTile extends StatelessWidget { +class EventAuthorTile extends ConsumerWidget { final EventOwner owner; const EventAuthorTile({super.key, required this.owner}); @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - width: 48, - height: 48, - color: AppColors.primary.withValues(alpha: 0.1), - child: owner.avatarUrl != null && owner.avatarUrl!.isNotEmpty - ? Image.network( - owner.avatarUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _defaultAvatar(); - }, - ) - : _defaultAvatar(), + Widget build(BuildContext context, WidgetRef ref) { + return InkWell( + onTap: () { + if (owner is User) { + final currentUser = ref.read(profileUserProvider); + final targetUserId = owner.id; + + if (currentUser != null && currentUser.id == targetUserId) { + Navigator.of(context).pushNamed(AppRoutes.profile); + } else { + Navigator.of( + context, + ).pushNamed(AppRoutes.profileDetails, arguments: targetUserId); + } + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 48, + height: 48, + color: AppColors.primary.withValues(alpha: 0.1), + child: owner.avatarUrl != null && owner.avatarUrl!.isNotEmpty + ? Image.network( + owner.avatarUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _defaultAvatar(); + }, + ) + : _defaultAvatar(), + ), ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - owner.displayName, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, - color: AppColors.black, + const SizedBox(width: 12), + Expanded( + child: Text( + owner.displayName, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + color: AppColors.black, + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/features/event/presentation/widgets/event_card.dart b/lib/src/features/event/presentation/widgets/event_card.dart index bdd4b51..12aab2a 100644 --- a/lib/src/features/event/presentation/widgets/event_card.dart +++ b/lib/src/features/event/presentation/widgets/event_card.dart @@ -10,7 +10,7 @@ import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dar class EventCard extends StatelessWidget { final Event event; - const EventCard({super.key, required this.event,}); + const EventCard({super.key, required this.event}); @override Widget build(BuildContext context) { @@ -34,7 +34,10 @@ class EventCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - EventDateTimeRow(date: event.start), + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: EventDateTimeRow(date: event.start), + ), EventPriceTag( isPaid: event.isPaid, paidText: localizations.paid, diff --git a/lib/src/features/event/presentation/widgets/event_data_time_row.dart b/lib/src/features/event/presentation/widgets/event_data_time_row.dart index 076fb65..d12b990 100644 --- a/lib/src/features/event/presentation/widgets/event_data_time_row.dart +++ b/lib/src/features/event/presentation/widgets/event_data_time_row.dart @@ -22,11 +22,14 @@ class EventDateTimeRow extends StatelessWidget { color: AppColors.grey, ), const SizedBox(width: 8), - Text( - dateText, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: AppColors.grey), + Flexible( + child: Text( + dateText, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: AppColors.grey), + overflow: TextOverflow.ellipsis, + ), ), ], ); diff --git a/lib/src/features/profile/data/data_sources/profile_data_source.dart b/lib/src/features/profile/data/data_sources/profile_data_source.dart index 7677741..8c96791 100644 --- a/lib/src/features/profile/data/data_sources/profile_data_source.dart +++ b/lib/src/features/profile/data/data_sources/profile_data_source.dart @@ -38,4 +38,9 @@ class ProfileDataSource { Future deleteUserRequest() async { await httpClient.post('/api/profile/delete-request'); } + + Future getUserProfile(int userId) async { + final response = await httpClient.get('/api/profile/$userId'); + return User.fromJson(response['data']); + } } diff --git a/lib/src/features/profile/data/repositories/remote_profile_repository.dart b/lib/src/features/profile/data/repositories/remote_profile_repository.dart index b2f0511..1d1f05d 100644 --- a/lib/src/features/profile/data/repositories/remote_profile_repository.dart +++ b/lib/src/features/profile/data/repositories/remote_profile_repository.dart @@ -25,5 +25,8 @@ class RemoteProfileRepository implements ProfileRepository { @override Future deleteUserRequest() => dataSource.deleteUserRequest(); + + @override + Future getUserProfile(int userId) => dataSource.getUserProfile(userId); } diff --git a/lib/src/features/profile/domain/providers/get_user_profile_usecase_provider.dart b/lib/src/features/profile/domain/providers/get_user_profile_usecase_provider.dart new file mode 100644 index 0000000..75df117 --- /dev/null +++ b/lib/src/features/profile/domain/providers/get_user_profile_usecase_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/profile/data/providers/profile_repository_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/usecases/get_user_profile_usecase.dart'; + +final getUserProfileUseCaseProvider = Provider((ref) { + final repository = ref.watch(profileRepositoryProvider); + return GetUserProfileUseCase(repository); +}); diff --git a/lib/src/features/profile/domain/repositories/profile_repository.dart b/lib/src/features/profile/domain/repositories/profile_repository.dart index ad441a6..7fa2f16 100644 --- a/lib/src/features/profile/domain/repositories/profile_repository.dart +++ b/lib/src/features/profile/domain/repositories/profile_repository.dart @@ -9,5 +9,6 @@ abstract class ProfileRepository { }); Future getProfile(); + Future getUserProfile(int id); Future deleteUserRequest(); } diff --git a/lib/src/features/profile/domain/usecases/get_user_profile_usecase.dart b/lib/src/features/profile/domain/usecases/get_user_profile_usecase.dart new file mode 100644 index 0000000..b713431 --- /dev/null +++ b/lib/src/features/profile/domain/usecases/get_user_profile_usecase.dart @@ -0,0 +1,12 @@ +import 'package:interns2025b_mobile/src/features/profile/domain/repositories/profile_repository.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; + +class GetUserProfileUseCase { + final ProfileRepository repository; + + GetUserProfileUseCase(this.repository); + + Future call(int id) { + return repository.getUserProfile(id); + } +} diff --git a/lib/src/features/profile/presentation/controllers/profile_controller.dart b/lib/src/features/profile/presentation/controllers/profile_controller.dart index 677e935..f8a1e89 100644 --- a/lib/src/features/profile/presentation/controllers/profile_controller.dart +++ b/lib/src/features/profile/presentation/controllers/profile_controller.dart @@ -129,6 +129,10 @@ class ProfileController extends ChangeNotifier { } } + Future loadUser(BuildContext context) async { + await fetchUserProfile(context: context); + } + Future deleteUser({required BuildContext context}) async { _setLoading(true); diff --git a/lib/src/features/profile/presentation/controllers/profile_details_controller.dart b/lib/src/features/profile/presentation/controllers/profile_details_controller.dart new file mode 100644 index 0000000..554f0a3 --- /dev/null +++ b/lib/src/features/profile/presentation/controllers/profile_details_controller.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/usecases/get_profile_usecase.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/usecases/get_user_profile_usecase.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_details_user_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_user_provider.dart'; + +class ProfileDetailsController extends ChangeNotifier { + final Ref ref; + final GetUserProfileUseCase getUserProfileUseCase; + final GetProfileUseCase getProfileUseCase; + + ProfileDetailsController( + this.ref, + this.getUserProfileUseCase, + this.getProfileUseCase, + ); + + bool _isLoading = false; + bool get isLoading => _isLoading; + + Future loadUserById(int userId, BuildContext context) async { + _isLoading = true; + notifyListeners(); + + final currentUser = ref.read(profileUserProvider); + final isSelf = currentUser != null && currentUser.id == userId; + + final user = isSelf + ? await getProfileUseCase() + : await getUserProfileUseCase(userId); + + ref.read(profileDetailsUserProvider.notifier).state = user; + + } +} diff --git a/lib/src/features/profile/presentation/pages/profile_details_page.dart b/lib/src/features/profile/presentation/pages/profile_details_page.dart new file mode 100644 index 0000000..4d4b479 --- /dev/null +++ b/lib/src/features/profile/presentation/pages/profile_details_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/user_profile_by_id_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_info_card.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/navigation_bar.dart'; + +class ProfileDetailsPage extends ConsumerWidget { + const ProfileDetailsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userId = ModalRoute.of(context)!.settings.arguments as int; + final topPadding = MediaQuery.of(context).padding.top; + final localization = AppLocalizations.of(context)!.failedToLoadUserProfile; + + final userAsyncValue = ref.watch(userProfileByIdProvider(userId)); + + return userAsyncValue.when( + data: (user) { + return Scaffold( + body: Stack( + children: [ + Container(color: AppColors.primary), + Positioned.fill( + top: 0, + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: topPadding + 100), + ProfileInfoCard(user: user, editable: false), + ], + ), + ), + ), + ], + ), + bottomNavigationBar: const NavigationBarWidget(), + ); + }, + loading: () => + const Scaffold(body: Center(child: CircularProgressIndicator())), + error: (error, _) => Scaffold( + body: Center( + child: Text(localization.replaceAll('{e}', error.toString())), + ), + ), + ); + } +} diff --git a/lib/src/features/profile/presentation/pages/profile_page.dart b/lib/src/features/profile/presentation/pages/profile_page.dart index 3484f90..67a57ac 100644 --- a/lib/src/features/profile/presentation/pages/profile_page.dart +++ b/lib/src/features/profile/presentation/pages/profile_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_info_card.dart'; import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; import 'package:interns2025b_mobile/src/shared/presentation/widgets/navigation_bar.dart'; @@ -10,22 +12,42 @@ class ProfilePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final topPadding = MediaQuery.of(context).padding.top; + final user = ref.watch(profileControllerProvider).user; + final profileInit = ref.watch(profileInitProvider); + final localization = AppLocalizations.of(context)!; return Scaffold( body: Stack( children: [ Container(color: AppColors.primary), - - Positioned.fill( - top: 0, - child: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: topPadding + 100), - const ProfileInfoCard(), - ], + profileInit.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Text( + localization.failedToLoadUserProfile.replaceAll( + '{e}', + error.toString(), + ), + style: TextStyle(color: AppColors.red), ), ), + data: (_) { + if (user == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Positioned.fill( + top: 0, + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: topPadding + 100), + ProfileInfoCard(user: user, editable: true), + ], + ), + ), + ); + }, ), ], ), diff --git a/lib/src/features/profile/presentation/providers/profile_controller_provider.dart b/lib/src/features/profile/presentation/providers/profile_controller_provider.dart index 573ff1d..02ded81 100644 --- a/lib/src/features/profile/presentation/providers/profile_controller_provider.dart +++ b/lib/src/features/profile/presentation/providers/profile_controller_provider.dart @@ -17,3 +17,7 @@ final profileControllerProvider = ChangeNotifierProvider(( deleteUserRequestUseCase, ); }); + +final profileInitProvider = FutureProvider((ref) async { + await ref.read(profileControllerProvider.notifier).fetchUserProfile(); +}); diff --git a/lib/src/features/profile/presentation/providers/profile_details_controller_provider.dart b/lib/src/features/profile/presentation/providers/profile_details_controller_provider.dart new file mode 100644 index 0000000..6aa86e5 --- /dev/null +++ b/lib/src/features/profile/presentation/providers/profile_details_controller_provider.dart @@ -0,0 +1,17 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/providers/get_profile_usecase_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/providers/get_user_profile_usecase_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/controllers/profile_details_controller.dart'; + +final profileDetailsControllerProvider = +ChangeNotifierProvider((ref) { + final getUserProfileUseCase = ref.watch(getUserProfileUseCaseProvider); + final getProfileUseCase = ref.watch(getProfileUseCaseProvider); + return ProfileDetailsController( + ref, + getUserProfileUseCase, + getProfileUseCase, + ); +}); + + diff --git a/lib/src/features/profile/presentation/providers/profile_details_user_provider.dart b/lib/src/features/profile/presentation/providers/profile_details_user_provider.dart new file mode 100644 index 0000000..1c2ed16 --- /dev/null +++ b/lib/src/features/profile/presentation/providers/profile_details_user_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; + +final profileDetailsUserProvider = StateProvider((ref) => null); diff --git a/lib/src/features/profile/presentation/providers/user_profile_by_id_provider.dart b/lib/src/features/profile/presentation/providers/user_profile_by_id_provider.dart new file mode 100644 index 0000000..3b7e8bb --- /dev/null +++ b/lib/src/features/profile/presentation/providers/user_profile_by_id_provider.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/providers/get_user_profile_usecase_provider.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; + +final userProfileByIdProvider = FutureProvider.family(( + ref, + userId, +) async { + final useCase = ref.watch(getUserProfileUseCaseProvider); + return useCase(userId); +}); diff --git a/lib/src/features/profile/presentation/widgets/profile_details/profile_details_events.dart b/lib/src/features/profile/presentation/widgets/profile_details/profile_details_events.dart new file mode 100644 index 0000000..979e870 --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/profile_details/profile_details_events.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:interns2025b_mobile/src/core/routes/app_routes.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_card.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; + +class ProfileDetailsEvents extends StatelessWidget { + final List events; + + const ProfileDetailsEvents({super.key, required this.events}); + + @override + Widget build(BuildContext context) { + return Column( + children: events.map((e) { + return GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.eventDetails, + arguments: e.id, + ), + child: EventCard(event: e), + ); + }).toList(), + ); + } +} diff --git a/lib/src/features/profile/presentation/widgets/profile_details/profile_details_header.dart b/lib/src/features/profile/presentation/widgets/profile_details/profile_details_header.dart new file mode 100644 index 0000000..4717197 --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/profile_details/profile_details_header.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; + +class ProfileDetailsHeader extends StatelessWidget { + final User user; + + const ProfileDetailsHeader({super.key, required this.user}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (user.avatarUrl != null) + CircleAvatar( + radius: 50, + backgroundImage: NetworkImage(user.avatarUrl!), + ), + const SizedBox(height: 16), + Text(user.firstName, style: Theme.of(context).textTheme.headlineSmall), + if ((user.lastName?.trim().isNotEmpty ?? false)) + Text( + user.lastName!, + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ); + } +} diff --git a/lib/src/features/profile/presentation/widgets/profile_header.dart b/lib/src/features/profile/presentation/widgets/profile_header.dart deleted file mode 100644 index 88ca95c..0000000 --- a/lib/src/features/profile/presentation/widgets/profile_header.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; - -class ProfileHeader extends ConsumerWidget { - const ProfileHeader({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: AppColors.primary, - statusBarIconBrightness: Brightness.light, - ), - child: Container( - height: 100, - decoration: const BoxDecoration(color: AppColors.primary), - child: const SizedBox(), - ), - ); - } -} diff --git a/lib/src/features/profile/presentation/widgets/profile_info_card.dart b/lib/src/features/profile/presentation/widgets/profile_info_card.dart index 12d42a5..d1fbc98 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_card.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_card.dart @@ -1,54 +1,59 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/avatar_widget.dart'; -import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_edit_section.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_info_content.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; -class ProfileInfoCard extends ConsumerWidget { - const ProfileInfoCard({super.key}); +class ProfileInfoCard extends StatelessWidget { + final User? user; + final bool editable; + + const ProfileInfoCard({super.key, this.user, this.editable = true}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { + final effectiveUser = user; + + if (effectiveUser == null) return const SizedBox(); + return Stack( clipBehavior: Clip.none, children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(24, 60, 24, 24), - decoration: BoxDecoration( - color: AppColors.backgroundLight, - borderRadius: const BorderRadius.vertical(top: Radius.circular(50)), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 12, - offset: Offset(0, -2), + LayoutBuilder( + builder: (context, constraints) { + final screenHeight = MediaQuery.of(context).size.height; + final navBarHeight = kBottomNavigationBarHeight; + + return Container( + constraints: BoxConstraints( + minHeight: screenHeight - navBarHeight, + ), + width: double.infinity, + padding: const EdgeInsets.fromLTRB(24, 60, 24, 24), + decoration: BoxDecoration( + color: AppColors.backgroundLight, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(50), + ), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: ProfileInfoContent( + user: effectiveUser, + editable: editable, ), - ], - ), - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: const [ - ProfileInfoContent(), - SizedBox(height: 24), - ProfileEditSection(), - ], - ), - ), + ); + }, ), Positioned( top: -50, left: (MediaQuery.of(context).size.width - 100) / 2, - child: Consumer( - builder: (context, ref, _) { - final user = ref.watch(profileControllerProvider).user; - return AvatarWidget(avatarUrl: user?.avatarUrl); - }, - ), + child: AvatarWidget(avatarUrl: effectiveUser.avatarUrl), ), ], ); diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 084a3d1..95e6572 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -1,19 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:interns2025b_mobile/src/features/profile/domain/utils/event_sorter.dart'; -import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_edit_section.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_events_section.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_stats.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; -class ProfileInfoContent extends ConsumerWidget { - const ProfileInfoContent({super.key}); +class ProfileInfoContent extends StatelessWidget { + final User user; + final bool editable; - @override - Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(profileControllerProvider).user; - - if (user == null) return const SizedBox(); + const ProfileInfoContent({ + super.key, + required this.user, + this.editable = true, + }); + @override + Widget build(BuildContext context) { final sortedEvents = [...user.events]; sortEvents(sortedEvents); @@ -23,22 +26,26 @@ class ProfileInfoContent extends ConsumerWidget { const SizedBox(height: 24), Text( user.firstName, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), if ((user.lastName?.trim().isNotEmpty ?? false)) Text( user.lastName!, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 24), - const ProfileStats(), + ProfileStats(user: user), ProfileEventsSection(events: sortedEvents), + if (editable) ...[ + const SizedBox(height: 24), + const ProfileEditSection(), + ], ], ); } diff --git a/lib/src/features/profile/presentation/widgets/profile_stats.dart b/lib/src/features/profile/presentation/widgets/profile_stats.dart index a81385f..462c559 100644 --- a/lib/src/features/profile/presentation/widgets/profile_stats.dart +++ b/lib/src/features/profile/presentation/widgets/profile_stats.dart @@ -1,19 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; -import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/stat_tile.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; -class ProfileStats extends ConsumerWidget { - const ProfileStats({super.key}); +class ProfileStats extends StatelessWidget { + final User user; + + const ProfileStats({super.key, required this.user}); @override - Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(profileControllerProvider).user; + Widget build(BuildContext context) { final localizations = AppLocalizations.of(context)!; - if (user == null) return const SizedBox(); - return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -28,6 +26,7 @@ class ProfileStats extends ConsumerWidget { label: localizations.followingCount, value: user.followingCount, ), + const SizedBox(height: 16), ], ); } diff --git a/lib/src/shared/domain/models/event_owner.dart b/lib/src/shared/domain/models/event_owner.dart index e811f49..d87912b 100644 --- a/lib/src/shared/domain/models/event_owner.dart +++ b/lib/src/shared/domain/models/event_owner.dart @@ -1,4 +1,5 @@ abstract class EventOwner { + int get id; String get displayName; String? get avatarUrl; } diff --git a/lib/src/shared/domain/models/organization_model.dart b/lib/src/shared/domain/models/organization_model.dart index 34d86af..dbd596b 100644 --- a/lib/src/shared/domain/models/organization_model.dart +++ b/lib/src/shared/domain/models/organization_model.dart @@ -1,6 +1,7 @@ import 'package:interns2025b_mobile/src/shared/domain/models/event_owner.dart'; class Organization implements EventOwner { + @override final int id; final String name; final String? groupUrl; diff --git a/lib/src/shared/domain/models/user_model.dart b/lib/src/shared/domain/models/user_model.dart index ae410f8..3f4c209 100644 --- a/lib/src/shared/domain/models/user_model.dart +++ b/lib/src/shared/domain/models/user_model.dart @@ -2,6 +2,7 @@ import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_owner.dart'; class User implements EventOwner { + @override final int id; final String firstName; final String? lastName;