diff --git a/lib/features/messaging/presentation/screens/messaging_shell.dart b/lib/core/widgets/app_bottom_nav_bar.dart similarity index 59% rename from lib/features/messaging/presentation/screens/messaging_shell.dart rename to lib/core/widgets/app_bottom_nav_bar.dart index 35d6493..5f4baba 100644 --- a/lib/features/messaging/presentation/screens/messaging_shell.dart +++ b/lib/core/widgets/app_bottom_nav_bar.dart @@ -4,31 +4,23 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; -class MessagingShell extends ConsumerWidget { - final Widget child; +/// Determines which tab is active based on the current route. +int _currentTabIndex(BuildContext context) { + final location = GoRouterState.of(context).matchedLocation; + if (location.startsWith('/messages')) return 2; + if (location.startsWith('/agents/search')) return 1; + if (location.startsWith('/profile')) return 3; + return 0; // home and everything else +} - const MessagingShell({super.key, required this.child}); +class AppBottomNavBar extends ConsumerWidget { + const AppBottomNavBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - SafeArea( - bottom: false, - child: const HomeHeader(), - ), - Expanded(child: child), - ], - ), - bottomNavigationBar: _buildBottomNavBar(context, ref), - ); - } + final selectedIndex = _currentTabIndex(context); - Widget _buildBottomNavBar(BuildContext context, WidgetRef ref) { return Container( decoration: const BoxDecoration( color: Colors.white, @@ -43,39 +35,42 @@ class MessagingShell extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildNavItem( - context: context, + _NavItem( svgPath: 'assets/icons/nav_home_icon.svg', fallbackIcon: Icons.home, - isSelected: false, + isSelected: selectedIndex == 0, onTap: () => context.go('/home'), ), - _buildNavItem( - context: context, + _NavItem( svgPath: 'assets/icons/nav_list_icon.svg', fallbackIcon: Icons.list, - isSelected: false, - onTap: () => context.push('/agents/search'), + isSelected: selectedIndex == 1, + onTap: () => context.go('/agents/search'), ), - _buildNavItem( - context: context, + _NavItem( svgPath: 'assets/icons/nav_messages_icon.svg', fallbackIcon: Icons.chat_bubble, - isSelected: true, - onTap: () => context.go('/messages'), - ), - _buildNavItem( - context: context, - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: false, + isSelected: selectedIndex == 2, onTap: () { final authState = ref.read(authProvider); if (authState.status != AuthStatus.authenticated) { context.push('/login'); return; } - context.push('/profile'); + context.go('/messages'); + }, + ), + _NavItem( + svgPath: 'assets/icons/nav_profile_icon.svg', + fallbackIcon: Icons.person_outline, + isSelected: selectedIndex == 3, + onTap: () { + final authState = ref.read(authProvider); + if (authState.status != AuthStatus.authenticated) { + context.push('/login'); + return; + } + context.go('/profile'); }, ), ], @@ -84,14 +79,23 @@ class MessagingShell extends ConsumerWidget { ), ); } +} - Widget _buildNavItem({ - required BuildContext context, - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { +class _NavItem extends StatelessWidget { + final String svgPath; + final IconData fallbackIcon; + final bool isSelected; + final VoidCallback onTap; + + const _NavItem({ + required this.svgPath, + required this.fallbackIcon, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, @@ -102,7 +106,8 @@ class MessagingShell extends ConsumerWidget { width: 24, height: 24, colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) + ? const ColorFilter.mode( + AppColors.primaryDark, BlendMode.srcIn) : const ColorFilter.mode( AppColors.accentOrange, BlendMode.srcIn), placeholderBuilder: (_) => Icon( diff --git a/lib/core/widgets/app_shell.dart b/lib/core/widgets/app_shell.dart new file mode 100644 index 0000000..ea1ca7a --- /dev/null +++ b/lib/core/widgets/app_shell.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:real_estate_mobile/core/widgets/app_bottom_nav_bar.dart'; +import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; + +/// Root shell that provides the persistent header and bottom nav bar. +/// Only the content area (child) swaps when navigating between tabs. +class AppShell extends StatelessWidget { + final Widget child; + + const AppShell({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Column( + children: [ + SafeArea( + bottom: false, + child: const HomeHeader(), + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + child: KeyedSubtree( + key: ValueKey(child.runtimeType), + child: child, + ), + ), + ), + ], + ), + bottomNavigationBar: const AppBottomNavBar(), + ); + } +} diff --git a/lib/features/about/presentation/screens/about_screen.dart b/lib/features/about/presentation/screens/about_screen.dart index 898e2b2..fe48692 100644 --- a/lib/features/about/presentation/screens/about_screen.dart +++ b/lib/features/about/presentation/screens/about_screen.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/network/api_client.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; // ── Data models (matching web CMS types) ── @@ -305,56 +302,44 @@ class AboutScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(_aboutProvider); - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - bottom: false, - child: Column( - children: [ - const HomeHeader(), - Expanded( - child: state.isLoading - ? const Center( - child: CircularProgressIndicator( - color: AppColors.accentOrange, - ), - ) - : SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - const SizedBox(height: 18), - _buildHeroTitle(state.hero.headline), - const SizedBox(height: 16), - _buildHeroSubtitle(state.hero.description), - const SizedBox(height: 24), - _buildBannerImage(state.hero.bannerImageUrl), - const SizedBox(height: 10), - _buildBannerCaption(state.hero.bannerOverlayText), - const SizedBox(height: 16), - _buildStatsRow(state.stats), - const SizedBox(height: 32), - _buildSectionTitle(state.featuresBadge), - const SizedBox(height: 12), - _buildWhyChooseUsSubtitle(), - const SizedBox(height: 24), - ...state.features.map((f) => Padding( - padding: const EdgeInsets.only(bottom: 15), - child: _buildFeatureCard(f), - )), - const SizedBox(height: 25), - _buildTeamSection(context, ref, state), - const SizedBox(height: 40), - _buildCtaSection(context, state.cta), - const SizedBox(height: 30), - ], - ), - ), - ), - ], + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.accentOrange, ), + ); + } + + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + const SizedBox(height: 18), + _buildHeroTitle(state.hero.headline), + const SizedBox(height: 16), + _buildHeroSubtitle(state.hero.description), + const SizedBox(height: 24), + _buildBannerImage(state.hero.bannerImageUrl), + const SizedBox(height: 10), + _buildBannerCaption(state.hero.bannerOverlayText), + const SizedBox(height: 16), + _buildStatsRow(state.stats), + const SizedBox(height: 32), + _buildSectionTitle(state.featuresBadge), + const SizedBox(height: 12), + _buildWhyChooseUsSubtitle(), + const SizedBox(height: 24), + ...state.features.map((f) => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildFeatureCard(f), + )), + const SizedBox(height: 25), + _buildTeamSection(context, ref, state), + const SizedBox(height: 40), + _buildCtaSection(context, state.cta), + const SizedBox(height: 30), + ], ), - bottomNavigationBar: _buildBottomNavBar(context, ref), ); } @@ -854,105 +839,6 @@ class AboutScreen extends ConsumerWidget { ); } - // -- Bottom Navigation Bar -- - Widget _buildBottomNavBar(BuildContext context, WidgetRef ref) { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - context: context, - ref: ref, - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - isSelected: false, - onTap: () => context.go('/home'), - ), - _buildNavItem( - context: context, - ref: ref, - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: false, - onTap: () => context.push('/agents/search'), - ), - _buildNavItem( - context: context, - ref: ref, - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.go('/messages'); - }, - ), - _buildNavItem( - context: context, - ref: ref, - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required BuildContext context, - required WidgetRef ref, - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode( - AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } class _TeamMemberPlaceholder extends StatelessWidget { diff --git a/lib/features/agents/data/agents_repository.dart b/lib/features/agents/data/agents_repository.dart index a42242f..6bdbfb9 100644 --- a/lib/features/agents/data/agents_repository.dart +++ b/lib/features/agents/data/agents_repository.dart @@ -195,6 +195,33 @@ class AgentsRepository { } } + /// Get received connection requests: GET /connection-requests/received?status=... + Future>> getReceivedRequests(String status) async { + try { + final response = await _dio.get( + '${ApiConstants.connectionRequests}/received', + queryParameters: {'status': status}, + ); + final data = response.data['data'] as List? ?? []; + return data.cast>(); + } on DioException catch (_) { + return []; + } + } + + /// Respond to connection request: PATCH /connection-requests/{id}/respond + Future respondToRequest(String requestId, String status) async { + try { + await _dio.patch( + '${ApiConstants.connectionRequests}/$requestId/respond', + data: {'status': status}, + ); + return true; + } on DioException catch (_) { + return false; + } + } + /// Get agent types: GET /agent-types /// Response: { success, data: [...] } Future> getAgentTypes() async { diff --git a/lib/features/agents/presentation/providers/agent_network_provider.dart b/lib/features/agents/presentation/providers/agent_network_provider.dart new file mode 100644 index 0000000..c92f344 --- /dev/null +++ b/lib/features/agents/presentation/providers/agent_network_provider.dart @@ -0,0 +1,124 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:real_estate_mobile/features/agents/data/agents_repository.dart'; + +class AgentNetworkState { + final List> pendingRequests; + final List> connections; + final bool isLoading; + final String? error; + final Set processingIds; + + const AgentNetworkState({ + this.pendingRequests = const [], + this.connections = const [], + this.isLoading = true, + this.error, + this.processingIds = const {}, + }); + + AgentNetworkState copyWith({ + List>? pendingRequests, + List>? connections, + bool? isLoading, + String? error, + Set? processingIds, + bool clearError = false, + }) { + return AgentNetworkState( + pendingRequests: pendingRequests ?? this.pendingRequests, + connections: connections ?? this.connections, + isLoading: isLoading ?? this.isLoading, + error: clearError ? null : (error ?? this.error), + processingIds: processingIds ?? this.processingIds, + ); + } +} + +class AgentNetworkNotifier extends StateNotifier { + final AgentsRepository _repo; + + AgentNetworkNotifier(this._repo) : super(const AgentNetworkState()) { + loadData(); + } + + Future loadData() async { + state = state.copyWith(isLoading: true, clearError: true); + try { + final results = await Future.wait([ + _repo.getReceivedRequests('PENDING'), + _repo.getReceivedRequests('ACCEPTED'), + ]); + state = state.copyWith( + pendingRequests: results[0], + connections: results[1], + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: 'Failed to load network data', + ); + } + } + + Future acceptRequest(String requestId) async { + if (state.processingIds.contains(requestId)) return; + state = state.copyWith( + processingIds: {...state.processingIds, requestId}, + ); + + final success = await _repo.respondToRequest(requestId, 'ACCEPTED'); + if (success) { + // Move from pending to connections + final request = state.pendingRequests.firstWhere( + (r) => r['id'] == requestId, + orElse: () => {}, + ); + state = state.copyWith( + pendingRequests: + state.pendingRequests.where((r) => r['id'] != requestId).toList(), + connections: request.isNotEmpty + ? [request, ...state.connections] + : state.connections, + processingIds: state.processingIds + .where((id) => id != requestId) + .toSet(), + ); + } else { + state = state.copyWith( + processingIds: state.processingIds + .where((id) => id != requestId) + .toSet(), + ); + } + } + + Future rejectRequest(String requestId) async { + if (state.processingIds.contains(requestId)) return; + state = state.copyWith( + processingIds: {...state.processingIds, requestId}, + ); + + final success = await _repo.respondToRequest(requestId, 'REJECTED'); + if (success) { + state = state.copyWith( + pendingRequests: + state.pendingRequests.where((r) => r['id'] != requestId).toList(), + processingIds: state.processingIds + .where((id) => id != requestId) + .toSet(), + ); + } else { + state = state.copyWith( + processingIds: state.processingIds + .where((id) => id != requestId) + .toSet(), + ); + } + } +} + +final agentNetworkProvider = + StateNotifierProvider.autoDispose( + (ref) => AgentNetworkNotifier(AgentsRepository()), +); diff --git a/lib/features/agents/presentation/screens/agent_detail_screen.dart b/lib/features/agents/presentation/screens/agent_detail_screen.dart index b43a999..c8c7b15 100644 --- a/lib/features/agents/presentation/screens/agent_detail_screen.dart +++ b/lib/features/agents/presentation/screens/agent_detail_screen.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; class AgentDetailScreen extends ConsumerStatefulWidget { final String agentId; @@ -41,16 +39,14 @@ class _AgentDetailScreenState extends ConsumerState { Widget build(BuildContext context) { final state = ref.watch(agentDetailProvider(widget.agentId)); - return Scaffold( - backgroundColor: Colors.white, - body: state.isLoading - ? const Center( - child: CircularProgressIndicator(color: AppColors.accentOrange)) - : state.error != null - ? _buildError(state.error!) - : _buildContent(state), - bottomNavigationBar: _buildBottomNavBar(), - ); + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(color: AppColors.accentOrange)); + } + if (state.error != null) { + return _buildError(state.error!); + } + return _buildContent(state); } Widget _buildError(String error) { @@ -86,8 +82,6 @@ class _AgentDetailScreenState extends ConsumerState { return SingleChildScrollView( child: Column( children: [ - SafeArea(bottom: false, child: const HomeHeader()), - // ── Avatar Section ── const SizedBox(height: 16), _buildAvatarSection(agent), @@ -1033,90 +1027,6 @@ class _AgentDetailScreenState extends ConsumerState { ); } - // ── Bottom Nav ── - Widget _buildBottomNavBar() { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Color(0xFFE8E8E8), width: 1)), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - onTap: () => context.go('/home'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: true, - onTap: () => context.go('/agents/search'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - onTap: () { - if (ref.read(authProvider).status != - AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.go('/messages'); - }, - ), - _buildNavItem( - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - onTap: () { - if (ref.read(authProvider).status != - AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required String svgPath, - required IconData fallbackIcon, - bool isSelected = false, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - isSelected ? AppColors.primaryDark : AppColors.accentOrange, - BlendMode.srcIn, - ), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } // ── Expandable Chips Section ── diff --git a/lib/features/agents/presentation/screens/agent_home_screen.dart b/lib/features/agents/presentation/screens/agent_home_screen.dart index 3c77cf1..7d1a288 100644 --- a/lib/features/agents/presentation/screens/agent_home_screen.dart +++ b/lib/features/agents/presentation/screens/agent_home_screen.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; /// Provider that fetches the agent's own profile ID from /agents/profile/me @@ -30,51 +27,42 @@ class AgentHomeScreen extends ConsumerWidget { final myProfileAsync = ref.watch(_agentMyProfileProvider); return myProfileAsync.when( - loading: () => const Scaffold( - backgroundColor: Colors.white, - body: Center( - child: CircularProgressIndicator( - color: AppColors.accentOrange, - strokeWidth: 2, - ), + loading: () => const Center( + child: CircularProgressIndicator( + color: AppColors.accentOrange, + strokeWidth: 2, ), ), - error: (_, __) => Scaffold( - backgroundColor: Colors.white, - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.error_outline, - size: 48, color: AppColors.accentOrange), - const SizedBox(height: 16), - const Text('Failed to load profile', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.primaryDark)), - const SizedBox(height: 12), - TextButton( - onPressed: () => ref.invalidate(_agentMyProfileProvider), - child: const Text('Retry', - style: TextStyle(color: AppColors.accentOrange)), - ), - ], - ), + error: (_, __) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, + size: 48, color: AppColors.accentOrange), + const SizedBox(height: 16), + const Text('Failed to load profile', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark)), + const SizedBox(height: 12), + TextButton( + onPressed: () => ref.invalidate(_agentMyProfileProvider), + child: const Text('Retry', + style: TextStyle(color: AppColors.accentOrange)), + ), + ], ), ), data: (agentProfileId) { if (agentProfileId == null) { - return const Scaffold( - backgroundColor: Colors.white, - body: Center( - child: Text('No agent profile found', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 16, - color: AppColors.primaryDark)), - ), + return const Center( + child: Text('No agent profile found', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + color: AppColors.primaryDark)), ); } return _AgentHomeContent(agentProfileId: agentProfileId); @@ -97,16 +85,14 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { Widget build(BuildContext context) { final state = ref.watch(agentDetailProvider(widget.agentProfileId)); - return Scaffold( - backgroundColor: Colors.white, - body: state.isLoading - ? const Center( - child: CircularProgressIndicator(color: AppColors.accentOrange)) - : state.error != null - ? _buildError(state.error!) - : _buildContent(state), - bottomNavigationBar: _buildBottomNavBar(), - ); + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(color: AppColors.accentOrange)); + } + if (state.error != null) { + return _buildError(state.error!); + } + return _buildContent(state); } Widget _buildError(String error) { @@ -140,8 +126,6 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { return SingleChildScrollView( child: Column( children: [ - SafeArea(bottom: false, child: const HomeHeader()), - const SizedBox(height: 12), _buildCoverAndAvatar(agent), const SizedBox(height: 16), _buildStatusSection(agent), @@ -968,97 +952,6 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { ); } - // ── Bottom Navigation Bar ── - Widget _buildBottomNavBar() { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - isSelected: true, - onTap: () {}, - ), - _buildNavItem( - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: false, - onTap: () => context.push('/agents/search'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/messages'); - }, - ), - _buildNavItem( - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode( - AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode( - AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: - isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } // ── Testimonial Card (same as agent detail screen) ── diff --git a/lib/features/agents/presentation/screens/agent_network_screen.dart b/lib/features/agents/presentation/screens/agent_network_screen.dart new file mode 100644 index 0000000..0db4bb8 --- /dev/null +++ b/lib/features/agents/presentation/screens/agent_network_screen.dart @@ -0,0 +1,521 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; +import 'package:real_estate_mobile/core/widgets/s3_image.dart'; +import 'package:real_estate_mobile/features/agents/presentation/providers/agent_network_provider.dart'; + +class AgentNetworkScreen extends ConsumerWidget { + const AgentNetworkScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(agentNetworkProvider); + + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(color: AppColors.accentOrange)); + } + if (state.error != null) { + return _buildError(context, ref, state.error!); + } + return _buildContent(context, ref, state); + } + + Widget _buildError(BuildContext context, WidgetRef ref, String error) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, + size: 48, color: AppColors.accentOrange), + const SizedBox(height: 16), + Text(error, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + )), + const SizedBox(height: 12), + TextButton( + onPressed: () => + ref.read(agentNetworkProvider.notifier).loadData(), + child: const Text('Try Again', + style: TextStyle(color: AppColors.accentOrange)), + ), + ], + ), + ); + } + + Widget _buildContent( + BuildContext context, WidgetRef ref, AgentNetworkState state) { + return RefreshIndicator( + color: AppColors.accentOrange, + onRefresh: () => ref.read(agentNetworkProvider.notifier).loadData(), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + + // ── Pending Requests Section ── + if (state.pendingRequests.isNotEmpty) ...[ + _buildSectionHeader('Pending Requests', + count: state.pendingRequests.length), + const SizedBox(height: 12), + ...state.pendingRequests.map((request) => _RequestCard( + data: request, + isProcessing: + state.processingIds.contains(request['id']), + )), + const SizedBox(height: 16), + Divider( + color: AppColors.primaryDark.withValues(alpha: 0.1), + height: 1), + const SizedBox(height: 16), + ], + + // ── Manage My Network Section ── + _buildManageNetworkHeader(context), + const SizedBox(height: 12), + + if (state.connections.isEmpty) + _buildEmptyState( + icon: Icons.people_outline, + message: 'No connections yet', + subtitle: + 'When you accept connection requests, they will appear here.', + ) + else + ...state.connections + .map((conn) => _ConnectionCard(data: conn)), + + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildSectionHeader(String title, {int? count}) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Text( + title, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + if (count != null && count > 0) ...[ + const SizedBox(width: 8), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '$count', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ], + ], + ), + ); + } + + Widget _buildManageNetworkHeader(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 0), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 0.5), + borderRadius: BorderRadius.circular(7), + ), + child: Row( + children: [ + const Text( + 'Manage My Network', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + const Spacer(), + Icon(Icons.arrow_forward, + size: 20, + color: AppColors.primaryDark.withValues(alpha: 0.7)), + ], + ), + ); + } + + Widget _buildEmptyState({ + required IconData icon, + required String message, + String? subtitle, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 48, color: AppColors.primaryDark.withValues(alpha: 0.3)), + const SizedBox(height: 12), + Text( + message, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 6), + Text( + subtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + color: AppColors.primaryDark.withValues(alpha: 0.6), + ), + ), + ], + ], + ), + ), + ); + } + +} + +// ── Relative time formatting (same as web) ── +String _formatRelativeTime(String? dateStr) { + if (dateStr == null || dateStr.isEmpty) return ''; + try { + final date = DateTime.parse(dateStr); + final now = DateTime.now(); + final diff = now.difference(date); + + if (diff.inSeconds < 60) return 'Just now'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; + if (diff.inHours < 24) return '${diff.inHours}h ago'; + if (diff.inDays < 7) return '${diff.inDays}d ago'; + return DateFormat('MMM d').format(date); + } catch (_) { + return ''; + } +} + +// ── Date formatting for connections ── +String _formatConnectedDate(String? dateStr) { + if (dateStr == null || dateStr.isEmpty) return ''; + try { + final date = DateTime.parse(dateStr); + return 'Connected on ${DateFormat('MMM d, y').format(date)}'; + } catch (_) { + return ''; + } +} + +// ── Request Card (pending connection requests with accept/reject) ── +class _RequestCard extends ConsumerWidget { + final Map data; + final bool isProcessing; + + const _RequestCard({required this.data, required this.isProcessing}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = data['user'] as Map? ?? {}; + final userName = + user['email'] as String? ?? user['name'] as String? ?? 'User'; + final avatar = user['avatar'] as String?; + final message = data['message'] as String?; + final requestId = data['id'] as String; + final createdAt = data['createdAt'] as String?; + final relativeTime = _formatRelativeTime(createdAt); + + return Opacity( + opacity: isProcessing ? 0.5 : 1.0, + child: AbsorbPointer( + absorbing: isProcessing, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + // Avatar + ClipOval( + child: SizedBox( + width: 56, + height: 56, + child: avatar != null && avatar.isNotEmpty + ? S3Image( + imageUrl: avatar, + width: 56, + height: 56, + fit: BoxFit.cover, + ) + : Container( + color: const Color(0xFFE0E0E0), + child: const Icon(Icons.person, + size: 28, color: AppColors.primaryDark), + ), + ), + ), + const SizedBox(width: 12), + // Name + message + time + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + userName, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Icon(Icons.verified_user_outlined, + size: 16, + color: AppColors.primaryDark + .withValues(alpha: 0.5)), + ], + ), + if (message != null && message.isNotEmpty) + Text( + message, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + color: AppColors.primaryDark, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Icon(Icons.link, + size: 14, + color: AppColors.primaryDark + .withValues(alpha: 0.5)), + const SizedBox(width: 4), + const Text( + 'Mutual Connection', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w200, + color: AppColors.primaryDark, + ), + ), + if (relativeTime.isNotEmpty) ...[ + const Spacer(), + Text( + relativeTime, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 12, + color: AppColors.primaryDark + .withValues(alpha: 0.5), + ), + ), + ], + ], + ), + ], + ), + ), + const SizedBox(width: 8), + // Reject button (X) + GestureDetector( + onTap: () => ref + .read(agentNetworkProvider.notifier) + .rejectRequest(requestId), + child: Container( + width: 33, + height: 31, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: + AppColors.primaryDark.withValues(alpha: 0.3), + width: 1), + ), + child: Icon(Icons.close, + size: 18, + color: + AppColors.primaryDark.withValues(alpha: 0.6)), + ), + ), + const SizedBox(width: 10), + // Accept button (checkmark) + GestureDetector( + onTap: () => ref + .read(agentNetworkProvider.notifier) + .acceptRequest(requestId), + child: Container( + width: 33, + height: 31, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.accentOrange, + ), + child: const Icon(Icons.check, + size: 20, color: Colors.white), + ), + ), + ], + ), + ), + ), + ); + } +} + +// ── Connection Card (accepted connections in Manage My Network) ── +class _ConnectionCard extends StatelessWidget { + final Map data; + + const _ConnectionCard({required this.data}); + + @override + Widget build(BuildContext context) { + final user = data['user'] as Map? ?? {}; + final userName = + user['email'] as String? ?? user['name'] as String? ?? 'User'; + final userEmail = user['email'] as String? ?? ''; + final avatar = user['avatar'] as String?; + final respondedAt = data['respondedAt'] as String?; + final connectedDate = _formatConnectedDate(respondedAt); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + // Avatar + ClipOval( + child: SizedBox( + width: 56, + height: 56, + child: avatar != null && avatar.isNotEmpty + ? S3Image( + imageUrl: avatar, + width: 56, + height: 56, + fit: BoxFit.cover, + ) + : Container( + color: const Color(0xFFE0E0E0), + child: const Icon(Icons.person, + size: 28, color: AppColors.primaryDark), + ), + ), + ), + const SizedBox(width: 12), + // Name + email + connected date + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + userName, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Icon(Icons.verified_user_outlined, + size: 16, + color: AppColors.primaryDark + .withValues(alpha: 0.5)), + ], + ), + if (userEmail.isNotEmpty) + Text( + userEmail, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 13, + color: + AppColors.primaryDark.withValues(alpha: 0.6), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (connectedDate.isNotEmpty) + Text( + connectedDate, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 12, + color: + AppColors.primaryDark.withValues(alpha: 0.5), + ), + ), + ], + ), + ), + const SizedBox(width: 8), + // Message button + GestureDetector( + onTap: () { + // Navigate to messages — if we have userId, could create/open conversation + context.push('/messages'); + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.accentOrange.withValues(alpha: 0.1), + ), + child: const Icon(Icons.chat_bubble_outline, + size: 18, color: AppColors.accentOrange), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/agents/presentation/screens/agent_search_screen.dart b/lib/features/agents/presentation/screens/agent_search_screen.dart index 081377d..aa94f83 100644 --- a/lib/features/agents/presentation/screens/agent_search_screen.dart +++ b/lib/features/agents/presentation/screens/agent_search_screen.dart @@ -8,8 +8,6 @@ import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dar import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart'; import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; class AgentSearchScreen extends ConsumerStatefulWidget { final String? initialQuery; @@ -79,23 +77,14 @@ class _AgentSearchScreenState extends ConsumerState { Widget build(BuildContext context) { final state = ref.watch(searchAgentsProvider); - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - // Shared header - SafeArea( - bottom: false, - child: const HomeHeader(), - ), - _buildSearchBar(), - const SizedBox(height: 12), - _buildFilterChips(state.agentTypes, state.selectedAgentTypeId), - const SizedBox(height: 12), - Expanded(child: _buildAgentList(state)), - ], - ), - bottomNavigationBar: _buildBottomNavBar(), + return Column( + children: [ + _buildSearchBar(), + const SizedBox(height: 12), + _buildFilterChips(state.agentTypes, state.selectedAgentTypeId), + const SizedBox(height: 12), + Expanded(child: _buildAgentList(state)), + ], ); } @@ -359,99 +348,6 @@ class _AgentSearchScreenState extends ConsumerState { ); } - // ── Bottom Navigation Bar (shared style matching HomeScreen) ── - Widget _buildBottomNavBar() { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - index: 0, - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - isSelected: false, - onTap: () => context.go('/home'), - ), - _buildNavItem( - index: 1, - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: true, - onTap: () {}, - ), - _buildNavItem( - index: 2, - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.go('/messages'); - }, - ), - _buildNavItem( - index: 3, - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required int index, - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode(AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } // --- Agent Card Widget matching Figma design --- diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index 8689de2..24f8c2c 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -51,7 +51,8 @@ class AuthState { class AuthNotifier extends StateNotifier { final AuthRepository _repository; - AuthNotifier(this._repository) : super(const AuthState()) { + AuthNotifier(this._repository) + : super(const AuthState(status: AuthStatus.loading)) { // Wire up force logout callback so API client can reset auth state // when token refresh fails ApiClient.onForceLogout = () { @@ -61,14 +62,14 @@ class AuthNotifier extends StateNotifier { } Future checkAuthStatus() async { + state = state.copyWith(status: AuthStatus.loading); + final token = await SecureStorage.getAccessToken(); if (token == null) { state = state.copyWith(status: AuthStatus.initial); return; } - state = state.copyWith(status: AuthStatus.loading); - try { final user = await _repository.getMe(); state = state.copyWith( diff --git a/lib/features/contact/presentation/screens/contact_screen.dart b/lib/features/contact/presentation/screens/contact_screen.dart index 3dc294a..c949cff 100644 --- a/lib/features/contact/presentation/screens/contact_screen.dart +++ b/lib/features/contact/presentation/screens/contact_screen.dart @@ -1,19 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; -class ContactScreen extends ConsumerStatefulWidget { +class ContactScreen extends StatefulWidget { const ContactScreen({super.key}); @override - ConsumerState createState() => _ContactScreenState(); + State createState() => _ContactScreenState(); } -class _ContactScreenState extends ConsumerState { +class _ContactScreenState extends State { final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); @@ -31,35 +27,21 @@ class _ContactScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - SafeArea( - bottom: false, - child: const HomeHeader(), - ), - Expanded( - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox(height: 18), - _buildTitle(), - const SizedBox(height: 10), - _buildDescription(), - const SizedBox(height: 40), - _buildContactForm(), - const SizedBox(height: 20), - _buildContactDetails(), - const SizedBox(height: 40), - _buildFindAgentSection(), - const SizedBox(height: 30), - ], - ), - ), - ], - ), - bottomNavigationBar: _buildBottomNavBar(), + return ListView( + padding: EdgeInsets.zero, + children: [ + const SizedBox(height: 18), + _buildTitle(), + const SizedBox(height: 10), + _buildDescription(), + const SizedBox(height: 40), + _buildContactForm(), + const SizedBox(height: 20), + _buildContactDetails(), + const SizedBox(height: 40), + _buildFindAgentSection(), + const SizedBox(height: 30), + ], ); } @@ -599,93 +581,4 @@ class _ContactScreenState extends ConsumerState { }); } - // -- Bottom Navigation Bar -- - Widget _buildBottomNavBar() { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - isSelected: false, - onTap: () => context.go('/home'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: false, - onTap: () => context.push('/agents/search'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.go('/messages'); - }, - ), - _buildNavItem( - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode( - AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } diff --git a/lib/features/faq/presentation/screens/faq_screen.dart b/lib/features/faq/presentation/screens/faq_screen.dart index 7b1d2a9..01d2822 100644 --- a/lib/features/faq/presentation/screens/faq_screen.dart +++ b/lib/features/faq/presentation/screens/faq_screen.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/network/api_client.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; // ── Data models ── @@ -181,153 +178,47 @@ class FaqScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(faqProvider); - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - SafeArea( - bottom: false, - child: const HomeHeader(), + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(color: AppColors.accentOrange), + ); + } + + return ListView( + padding: const EdgeInsets.fromLTRB(0, 20, 0, 30), + children: [ + // Title + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'Frequently Asked Questions ?', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 24, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + textAlign: TextAlign.center, ), - Expanded( - child: state.isLoading - ? const Center( - child: CircularProgressIndicator( - color: AppColors.accentOrange), - ) - : ListView( - padding: const EdgeInsets.fromLTRB(0, 20, 0, 30), - children: [ - // Title - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Text( - 'Frequently Asked Questions ?', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 24, - fontWeight: FontWeight.w700, - color: AppColors.primaryDark, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 24), + ), + const SizedBox(height: 24), - // FAQ items - ...state.faqs.map((faq) => _FaqAccordion( - faq: faq, - isExpanded: state.expandedId == faq.id, - onTap: () => - ref.read(faqProvider.notifier).toggleFaq(faq.id), - )), + // FAQ items + ...state.faqs.map((faq) => _FaqAccordion( + faq: faq, + isExpanded: state.expandedId == faq.id, + onTap: () => + ref.read(faqProvider.notifier).toggleFaq(faq.id), + )), - const SizedBox(height: 30), + const SizedBox(height: 30), - // Still Need Help section - const _StillNeedHelpSection(), - ], - ), - ), - ], - ), - bottomNavigationBar: _buildBottomNavBar(context, ref), + // Still Need Help section + const _StillNeedHelpSection(), + ], ); } - Widget _buildBottomNavBar(BuildContext context, WidgetRef ref) { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - context: context, - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - isSelected: false, - onTap: () => context.go('/home'), - ), - _buildNavItem( - context: context, - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: false, - onTap: () => context.push('/agents/search'), - ), - _buildNavItem( - context: context, - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.go('/messages'); - }, - ), - _buildNavItem( - context: context, - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required BuildContext context, - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode( - AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } // ── Accordion item ── diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart index f599196..0af0421 100644 --- a/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -1,68 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/featured_professionals_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/features_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/hero_section.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/testimonials_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/top_professionals_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/trust_stats_section.dart'; -class HomeScreen extends ConsumerStatefulWidget { +class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override - ConsumerState createState() => _HomeScreenState(); -} - -class _HomeScreenState extends ConsumerState { - int _selectedIndex = 0; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: _buildBody(), - bottomNavigationBar: _buildBottomNavBar(), - ); - } - - Widget _buildBody() { - // Only Home tab (index 0) has content for now - switch (_selectedIndex) { - case 0: - return _buildHomeContent(); - case 1: - case 2: - case 3: - default: - return Center( - child: Text( - ['Home', 'Listings', 'Messages', 'Profile'][_selectedIndex], - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.primaryDark, - ), - ), - ); - } - } - - Widget _buildHomeContent() { + Widget build(BuildContext context, WidgetRef ref) { return SingleChildScrollView( child: Column( children: [ - // Header - SafeArea( - bottom: false, - child: const HomeHeader(), - ), - // Hero Section with Search const HeroSection(), const SizedBox(height: 40), @@ -94,102 +47,6 @@ class _HomeScreenState extends ConsumerState { ); } - // ── Bottom Navigation Bar (matches Figma node 49:6485) ── - Widget _buildBottomNavBar() { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - index: 0, - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - ), - _buildNavItem( - index: 1, - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - ), - _buildNavItem( - index: 2, - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - ), - _buildNavItem( - index: 3, - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required int index, - required String svgPath, - required IconData fallbackIcon, - }) { - final isSelected = _selectedIndex == index; - - return GestureDetector( - onTap: () { - if (index == 1) { - context.push('/agents/search'); - return; - } - if (index == 2) { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/messages'); - return; - } - if (index == 3) { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.push('/profile'); - return; - } - setState(() => _selectedIndex = index); - }, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode(AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } - Widget _buildFooter() { return Container( width: double.infinity, diff --git a/lib/features/home/presentation/widgets/home_header.dart b/lib/features/home/presentation/widgets/home_header.dart index b111e9e..6efe483 100644 --- a/lib/features/home/presentation/widgets/home_header.dart +++ b/lib/features/home/presentation/widgets/home_header.dart @@ -68,6 +68,10 @@ class _MenuDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final authState = ref.watch(authProvider); + final isAuthenticated = authState.status == AuthStatus.authenticated; + final user = authState.user; + return DraggableScrollableSheet( initialChildSize: 0.85, minChildSize: 0.5, @@ -76,7 +80,7 @@ class _MenuDrawer extends ConsumerWidget { return Container( decoration: const BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: BorderRadius.vertical(top: Radius.circular(15)), ), child: Column( children: [ @@ -90,220 +94,284 @@ class _MenuDrawer extends ConsumerWidget { borderRadius: BorderRadius.circular(2), ), ), - const SizedBox(height: 16), - // Header with logo and close + const SizedBox(height: 12), + + // Header bar with logo and close button Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Image.asset( - 'assets/icons/logo.png', - width: 109, - height: 29, - ), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppColors.inputFill, - borderRadius: BorderRadius.circular(8), - ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 23, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFF648188), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Image.asset( + 'assets/icons/logo.png', + width: 109, + height: 29, + ), + GestureDetector( + onTap: () => Navigator.pop(context), child: const Icon( Icons.close, color: AppColors.primaryDark, - size: 20, + size: 24, ), ), - ), - ], + ], + ), ), ), - const SizedBox(height: 24), + + const SizedBox(height: 20), + + // Profile section (when authenticated) + if (isAuthenticated && user != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + // Avatar + Container( + width: 70, + height: 70, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.inputFill, + border: Border.all( + color: AppColors.divider, + width: 1, + ), + ), + child: ClipOval( + child: user.avatar != null + ? Image.network( + user.avatar!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon( + Icons.person, + size: 36, + color: AppColors.hintText, + ), + ) + : const Icon( + Icons.person, + size: 36, + color: AppColors.hintText, + ), + ), + ), + const SizedBox(width: 12), + // Name and welcome text + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Welcome Back,', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w300, + color: Colors.black, + ), + ), + const SizedBox(height: 2), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: user.firstName ?? '', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + TextSpan( + text: + ' ${user.lastName ?? ''}', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + ], + const Divider(color: AppColors.divider, height: 1), + // Menu items Expanded( child: ListView( controller: scrollController, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + padding: EdgeInsets.zero, children: [ _buildMenuItem( context, - icon: Icons.home_outlined, - label: 'Home', - onTap: () { - Navigator.pop(context); - context.go('/home'); - }, - ), - _buildMenuItem( - context, - icon: Icons.school_outlined, + icon: Icons.school_rounded, label: 'Education', onTap: () => Navigator.pop(context), ), + const Divider(color: AppColors.divider, height: 1), _buildMenuItem( context, - icon: Icons.info_outline, + icon: Icons.help_rounded, + label: "Faq's", + onTap: () { + Navigator.pop(context); + context.push('/faq'); + }, + ), + const Divider(color: AppColors.divider, height: 1), + _buildMenuItem( + context, + icon: Icons.info_rounded, label: 'About Us', onTap: () { Navigator.pop(context); context.push('/about'); }, ), + const Divider(color: AppColors.divider, height: 1), _buildMenuItem( context, - icon: Icons.help_outline, - label: "FAQ's", - onTap: () { - Navigator.pop(context); - context.push('/faq'); - }, - ), - _buildMenuItem( - context, - icon: Icons.mail_outline, - label: 'Contact', + icon: Icons.call_rounded, + label: 'Call Us', + showChevron: false, onTap: () { Navigator.pop(context); context.push('/contact'); }, ), - const SizedBox(height: 16), const Divider(color: AppColors.divider, height: 1), - const SizedBox(height: 16), - if (ref.watch(authProvider).status == - AuthStatus.authenticated) ...[ - _buildMenuItem( - context, - icon: Icons.person_outline, - label: 'Account Settings', - onTap: () { - Navigator.pop(context); - context.push('/profile'); - }, - ), - _buildMenuItem( - context, - icon: Icons.notifications_outlined, - label: 'Notification Settings', - onTap: () { - Navigator.pop(context); - context.push('/profile'); - }, - ), - const SizedBox(height: 16), - const Divider(color: AppColors.divider, height: 1), - const SizedBox(height: 24), - // Log Out button - SizedBox( + ], + ), + ), + + // Bottom section: Log Out or Login buttons + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: isAuthenticated + ? // Log Out button - filled orange + SizedBox( width: double.infinity, - child: OutlinedButton.icon( + height: 54, + child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); ref.read(authProvider.notifier).logout(); }, icon: const Icon( - Icons.logout, - color: AppColors.accentOrange, - size: 20, + Icons.logout_rounded, + color: AppColors.primaryDark, + size: 24, ), label: const Text( 'Log Out', style: TextStyle( fontFamily: 'Fractul', - fontSize: 16, + fontSize: 14, fontWeight: FontWeight.w700, - color: AppColors.accentOrange, - ), - ), - style: OutlinedButton.styleFrom( - padding: - const EdgeInsets.symmetric(vertical: 16), - side: const BorderSide( - color: AppColors.accentOrange, - width: 1.5, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - ), - ), - ] else ...[ - // Login button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - Navigator.pop(context); - context.push('/login'); - }, - icon: const Icon( - Icons.login, - color: Colors.white, - size: 20, - ), - label: const Text( - 'Log In', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 16, - fontWeight: FontWeight.w700, - color: Colors.white, + color: AppColors.primaryDark, ), ), style: ElevatedButton.styleFrom( backgroundColor: AppColors.accentOrange, - padding: - const EdgeInsets.symmetric(vertical: 16), + elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), ), ), + ) + : // Login / Sign Up buttons + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + context.push('/login'); + }, + icon: const Icon( + Icons.login, + color: Colors.white, + size: 20, + ), + label: const Text( + 'Log In', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.accentOrange, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + height: 54, + child: OutlinedButton.icon( + onPressed: () { + Navigator.pop(context); + context.push('/signup'); + }, + icon: const Icon( + Icons.person_add_outlined, + color: AppColors.accentOrange, + size: 20, + ), + label: const Text( + 'Sign Up', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.accentOrange, + ), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: AppColors.accentOrange, + width: 1.5, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + ], ), - const SizedBox(height: 12), - // Sign Up button - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () { - Navigator.pop(context); - context.push('/signup'); - }, - icon: const Icon( - Icons.person_add_outlined, - color: AppColors.accentOrange, - size: 20, - ), - label: const Text( - 'Sign Up', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 16, - fontWeight: FontWeight.w700, - color: AppColors.accentOrange, - ), - ), - style: OutlinedButton.styleFrom( - padding: - const EdgeInsets.symmetric(vertical: 16), - side: const BorderSide( - color: AppColors.accentOrange, - width: 1.5, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - ), - ), - ], - ], - ), ), ], ), @@ -317,31 +385,32 @@ class _MenuDrawer extends ConsumerWidget { required IconData icon, required String label, required VoidCallback onTap, + bool showChevron = true, }) { return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(10), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18), child: Row( children: [ - Icon(icon, color: AppColors.primaryDark, size: 24), - const SizedBox(width: 16), + Icon(icon, color: AppColors.accentOrange, size: 30), + const SizedBox(width: 20), Text( label, style: const TextStyle( fontFamily: 'Fractul', - fontSize: 16, - fontWeight: FontWeight.w600, + fontSize: 14, + fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const Spacer(), - const Icon( - Icons.chevron_right, - color: AppColors.hintText, - size: 22, - ), + if (showChevron) + const Icon( + Icons.chevron_right, + color: AppColors.hintText, + size: 22, + ), ], ), ), diff --git a/lib/features/messaging/presentation/screens/chat_screen.dart b/lib/features/messaging/presentation/screens/chat_screen.dart index 7181c91..a7e4c93 100644 --- a/lib/features/messaging/presentation/screens/chat_screen.dart +++ b/lib/features/messaging/presentation/screens/chat_screen.dart @@ -1201,56 +1201,57 @@ class _ChatScreenState extends ConsumerState { // Text input pill with send icon inside Expanded( - child: Container( + child: SizedBox( height: 44, - margin: const EdgeInsets.only(left: 4), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(22), - border: Border.all( + child: TextField( + controller: _messageController, + focusNode: _messageFocusNode, + textInputAction: TextInputAction.send, + onSubmitted: (_) => _sendMessage(), + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, color: AppColors.primaryDark, - width: 1.5, ), - ), - clipBehavior: Clip.antiAlias, - child: Row( - children: [ - Expanded( - child: TextField( - controller: _messageController, - focusNode: _messageFocusNode, - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendMessage(), - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.primaryDark, - ), - decoration: const InputDecoration( - hintText: 'Write a Message', - hintStyle: TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.primaryDark, - ), - contentPadding: EdgeInsets.symmetric( - horizontal: 18, - vertical: 10, - ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - ), + decoration: InputDecoration( + hintText: 'Write a Message', + hintStyle: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: const BorderSide( + color: AppColors.primaryDark, + width: 1.5, ), ), - // Send arrow inside the pill - GestureDetector( + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: const BorderSide( + color: AppColors.primaryDark, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: const BorderSide( + color: AppColors.primaryDark, + width: 1.5, + ), + ), + suffixIcon: GestureDetector( onTap: _sendMessage, behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.only(right: 14), + child: const Padding( + padding: EdgeInsets.only(right: 4), child: Icon( Icons.send, size: 20, @@ -1258,7 +1259,11 @@ class _ChatScreenState extends ConsumerState { ), ), ), - ], + suffixIconConstraints: const BoxConstraints( + minWidth: 40, + minHeight: 20, + ), + ), ), ), ), diff --git a/lib/features/messaging/presentation/screens/conversations_screen.dart b/lib/features/messaging/presentation/screens/conversations_screen.dart index a786e7b..2f95ceb 100644 --- a/lib/features/messaging/presentation/screens/conversations_screen.dart +++ b/lib/features/messaging/presentation/screens/conversations_screen.dart @@ -62,107 +62,159 @@ class _ConversationsScreenState extends ConsumerState { // -- Header with back arrow, search, icons -- Widget _buildHeader() { - return Container( + return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), - child: Container( - height: 48, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.primaryDark.withValues(alpha: 0.2), - width: 1, - ), - borderRadius: BorderRadius.circular(15), - ), - child: Row( - children: [ - // Back arrow - GestureDetector( - onTap: () { - if (_isSearchActive) { - setState(() { - _isSearchActive = false; - _searchController.clear(); - }); - } else { - context.go('/home'); - } - }, - behavior: HitTestBehavior.opaque, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 14), - child: Icon( - Icons.arrow_back_ios_new, - size: 16, - color: AppColors.primaryDark, - ), + child: Row( + children: [ + // Back arrow (outside the search box) + GestureDetector( + onTap: () { + if (_isSearchActive) { + setState(() { + _isSearchActive = false; + _searchController.clear(); + }); + } else { + context.go('/home'); + } + }, + behavior: HitTestBehavior.opaque, + child: const Padding( + padding: EdgeInsets.only(right: 10), + child: Icon( + Icons.arrow_back_ios_new, + size: 16, + color: AppColors.primaryDark, ), ), - // Search text or input - Expanded( - child: _isSearchActive - ? TextField( - controller: _searchController, - autofocus: true, - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 15, - fontWeight: FontWeight.w600, - color: AppColors.primaryDark, - ), - decoration: const InputDecoration( - hintText: 'Search Messages', - hintStyle: TextStyle( - fontFamily: 'Fractul', - fontSize: 15, - fontWeight: FontWeight.w600, - color: AppColors.hintText, - ), - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - isDense: true, - ), - ) - : GestureDetector( - onTap: () => setState(() => _isSearchActive = true), - child: const Text( - 'Search Messages', - style: TextStyle( + ), + // Search box with border + Expanded( + child: GestureDetector( + onTap: () { + if (!_isSearchActive) { + setState(() => _isSearchActive = true); + } + }, + child: SizedBox( + height: 44, + child: _isSearchActive + ? TextField( + controller: _searchController, + autofocus: true, + style: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), + decoration: InputDecoration( + hintText: 'Search Messages', + hintStyle: const TextStyle( + fontFamily: 'Fractul', + fontSize: 15, + fontWeight: FontWeight.w600, + color: AppColors.hintText, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide( + color: AppColors.primaryDark + .withValues(alpha: 0.25), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide( + color: AppColors.primaryDark + .withValues(alpha: 0.25), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide( + color: AppColors.primaryDark + .withValues(alpha: 0.4), + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close, size: 18), + color: AppColors.hintText, + onPressed: () { + _searchController.clear(); + setState(() {}); + }, + ) + : null, + ), + ) + : Container( + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.primaryDark + .withValues(alpha: 0.25), + ), + borderRadius: BorderRadius.circular(22), + ), + padding: + const EdgeInsets.symmetric(horizontal: 18), + child: const Text( + 'Search Messages', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 15, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), ), - ), - ), - // Search icon - GestureDetector( - onTap: () => setState(() => _isSearchActive = true), - behavior: HitTestBehavior.opaque, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Icon( - Icons.search, - size: 22, - color: AppColors.primaryDark, - ), ), ), - // Three dots menu - GestureDetector( - onTap: () {}, - behavior: HitTestBehavior.opaque, - child: const Padding( - padding: EdgeInsets.only(right: 14, left: 2), - child: Icon( - Icons.more_horiz, - size: 22, - color: AppColors.primaryDark, - ), + ), + const SizedBox(width: 10), + // Search icon (outside the search box) + GestureDetector( + onTap: () { + setState(() { + if (_isSearchActive) { + _isSearchActive = false; + _searchController.clear(); + } else { + _isSearchActive = true; + } + }); + }, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + _isSearchActive ? Icons.close : Icons.search, + size: 22, + color: AppColors.primaryDark, ), ), - ], - ), + ), + const SizedBox(width: 8), + // Three dots menu (outside the search box) + GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon( + Icons.more_horiz, + size: 22, + color: AppColors.primaryDark, + ), + ), + ), + ], ), ); } diff --git a/lib/features/profile/presentation/screens/profile_settings_screen.dart b/lib/features/profile/presentation/screens/profile_settings_screen.dart index f33a37d..5655f1b 100644 --- a/lib/features/profile/presentation/screens/profile_settings_screen.dart +++ b/lib/features/profile/presentation/screens/profile_settings_screen.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; -import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; -import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart'; class ProfileSettingsScreen extends ConsumerStatefulWidget { @@ -121,39 +118,27 @@ class _ProfileSettingsScreenState extends ConsumerState { } }); - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - SafeArea( - bottom: false, - child: const HomeHeader(), - ), - Expanded( - child: profileState.isLoading - ? const Center( - child: CircularProgressIndicator( - color: AppColors.accentOrange, - ), - ) - : Column( - children: [ - const SizedBox(height: 16), - _buildTabBar(), - _buildProgressBar(), - Expanded( - child: _selectedTab == 0 - ? _buildProfileSettingsTab(profileState) - : _selectedTab == 1 - ? _buildNotificationsTab() - : _buildPrivacyTab(), - ), - ], - ), - ), - ], - ), - bottomNavigationBar: _buildBottomNavBar(), + if (profileState.isLoading) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.accentOrange, + ), + ); + } + + return Column( + children: [ + const SizedBox(height: 16), + _buildTabBar(), + _buildProgressBar(), + Expanded( + child: _selectedTab == 0 + ? _buildProfileSettingsTab(profileState) + : _selectedTab == 1 + ? _buildNotificationsTab() + : _buildPrivacyTab(), + ), + ], ); } @@ -983,87 +968,4 @@ class _ProfileSettingsScreenState extends ConsumerState { ); } - // ── Bottom Navigation Bar ── - - Widget _buildBottomNavBar() { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 1), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavItem( - svgPath: 'assets/icons/nav_home_icon.svg', - fallbackIcon: Icons.home, - isSelected: false, - onTap: () => context.go('/home'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_list_icon.svg', - fallbackIcon: Icons.list, - isSelected: false, - onTap: () => context.push('/agents/search'), - ), - _buildNavItem( - svgPath: 'assets/icons/nav_messages_icon.svg', - fallbackIcon: Icons.chat_bubble, - isSelected: false, - onTap: () { - final authState = ref.read(authProvider); - if (authState.status != AuthStatus.authenticated) { - context.push('/login'); - return; - } - context.go('/messages'); - }, - ), - _buildNavItem( - svgPath: 'assets/icons/nav_profile_icon.svg', - fallbackIcon: Icons.person_outline, - isSelected: true, - onTap: () {}, - ), - ], - ), - ), - ), - ); - } - - Widget _buildNavItem({ - required String svgPath, - required IconData fallbackIcon, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SvgPicture.asset( - svgPath, - width: 24, - height: 24, - colorFilter: isSelected - ? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn) - : const ColorFilter.mode( - AppColors.accentOrange, BlendMode.srcIn), - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 24, - color: isSelected ? AppColors.primaryDark : AppColors.accentOrange, - ), - ), - ), - ); - } } diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index bc48250..8e1c66b 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; +import 'package:real_estate_mobile/core/widgets/app_shell.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/auth/presentation/screens/login_screen.dart'; import 'package:real_estate_mobile/features/auth/presentation/screens/signup_screen.dart'; @@ -10,16 +12,16 @@ import 'package:real_estate_mobile/features/faq/presentation/screens/faq_screen. import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart'; import 'package:real_estate_mobile/features/messaging/presentation/screens/conversations_screen.dart'; import 'package:real_estate_mobile/features/messaging/presentation/screens/chat_screen.dart'; -import 'package:real_estate_mobile/features/messaging/presentation/screens/messaging_shell.dart'; import 'package:real_estate_mobile/features/agents/presentation/screens/agent_home_screen.dart'; import 'package:real_estate_mobile/features/contact/presentation/screens/contact_screen.dart'; import 'package:real_estate_mobile/features/about/presentation/screens/about_screen.dart'; +import 'package:real_estate_mobile/features/agents/presentation/screens/agent_network_screen.dart'; import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart'; final routerProvider = Provider((ref) { final authRefreshNotifier = _AuthRefreshNotifier(); - ref.listen(authProvider, (_, __) { + ref.listen(authProvider, (previous, next) { authRefreshNotifier.notify(); }); @@ -60,6 +62,7 @@ final routerProvider = Provider((ref) { return null; }, routes: [ + // Auth routes (no shell — full screen) GoRoute( path: '/login', builder: (context, state) => const LoginScreen(), @@ -68,27 +71,29 @@ final routerProvider = Provider((ref) { path: '/signup', builder: (context, state) => const SignupScreen(), ), - GoRoute( - path: '/home', - builder: (context, state) => const _HomeRouteWrapper(), - ), - GoRoute( - path: '/agents/search', - builder: (context, state) { - final query = state.uri.queryParameters['q']; - return AgentSearchScreen(initialQuery: query); - }, - ), - GoRoute( - path: '/agents/detail/:id', - builder: (context, state) { - final id = state.pathParameters['id']!; - return AgentDetailScreen(agentId: id); - }, - ), + + // All main app routes wrapped in AppShell (shared header + bottom nav) ShellRoute( - builder: (context, state, child) => MessagingShell(child: child), + builder: (context, state, child) => AppShell(child: child), routes: [ + GoRoute( + path: '/home', + builder: (context, state) => const _HomeRouteWrapper(), + ), + GoRoute( + path: '/agents/search', + builder: (context, state) { + final query = state.uri.queryParameters['q']; + return AgentSearchScreen(initialQuery: query); + }, + ), + GoRoute( + path: '/agents/detail/:id', + builder: (context, state) { + final id = state.pathParameters['id']!; + return AgentDetailScreen(agentId: id); + }, + ), GoRoute( path: '/messages', builder: (context, state) => const ConversationsScreen(), @@ -100,35 +105,52 @@ final routerProvider = Provider((ref) { return ChatScreen(conversationId: id); }, ), + GoRoute( + path: '/faq', + builder: (context, state) => const FaqScreen(), + ), + GoRoute( + path: '/contact', + builder: (context, state) => const ContactScreen(), + ), + GoRoute( + path: '/about', + builder: (context, state) => const AboutScreen(), + ), + GoRoute( + path: '/agent/network', + builder: (context, state) => const AgentNetworkScreen(), + ), + GoRoute( + path: '/profile', + builder: (context, state) => const ProfileSettingsScreen(), + ), ], ), - GoRoute( - path: '/faq', - builder: (context, state) => const FaqScreen(), - ), - GoRoute( - path: '/contact', - builder: (context, state) => const ContactScreen(), - ), - GoRoute( - path: '/about', - builder: (context, state) => const AboutScreen(), - ), - GoRoute( - path: '/profile', - builder: (context, state) => const ProfileSettingsScreen(), - ), ], ); }); /// Reactively switches between HomeScreen and AgentHomeScreen based on auth. +/// Shows a loading indicator while auth status is being determined to prevent +/// flashing the wrong screen on refresh. class _HomeRouteWrapper extends ConsumerWidget { const _HomeRouteWrapper(); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider); + + // Show loader while auth is initializing or loading + if (authState.status == AuthStatus.initial || + authState.status == AuthStatus.loading) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.accentOrange, + ), + ); + } + if (authState.status == AuthStatus.authenticated && authState.user?.role == 'AGENT') { return const AgentHomeScreen(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f4a99c5..d38d7c2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin"); emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 512e235..0c49585 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST emoji_picker_flutter + file_selector_linux flutter_secure_storage_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 019226f..9382ca6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import emoji_picker_flutter +import file_selector_macos import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index c06980e..cba5723 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -281,6 +289,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -326,6 +366,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" flutter_riverpod: dependency: "direct main" description: @@ -480,6 +528,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 + url: "https://pub.dev" + source: hosted + version: "0.8.13+14" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 869b4b0..8ade334 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: intl: ^0.20.2 logger: ^2.5.0 flutter_dotenv: ^5.2.1 + image_picker: ^1.2.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 03aa6eb..f0f1fd9 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { EmojiPickerFlutterPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1edf968..c7ac99e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST emoji_picker_flutter + file_selector_windows flutter_secure_storage_windows url_launcher_windows )