diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 9e0b3b3..14c9b6b 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -56,18 +56,24 @@ class AppTheme { ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: AppColors.inputFill, + fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( horizontal: 24, vertical: 20, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(20), - borderSide: BorderSide.none, + borderSide: const BorderSide( + color: Color(0xFFE0E0E0), + width: 1, + ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(20), - borderSide: BorderSide.none, + borderSide: const BorderSide( + color: Color(0xFFE0E0E0), + width: 1, + ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(20), diff --git a/lib/features/agents/presentation/screens/agent_detail_screen.dart b/lib/features/agents/presentation/screens/agent_detail_screen.dart index ae2a933..b43a999 100644 --- a/lib/features/agents/presentation/screens/agent_detail_screen.dart +++ b/lib/features/agents/presentation/screens/agent_detail_screen.dart @@ -1067,6 +1067,7 @@ class _AgentDetailScreenState extends ConsumerState { context.push('/login'); return; } + context.go('/messages'); }, ), _buildNavItem( @@ -1078,6 +1079,7 @@ class _AgentDetailScreenState extends ConsumerState { context.push('/login'); return; } + context.push('/profile'); }, ), ], @@ -1162,46 +1164,50 @@ class _ExpandableChipsSectionState extends State<_ExpandableChipsSection> { spacing: 10, runSpacing: 10, children: [ - ...showItems.map((item) => Container( - height: 28, - padding: const EdgeInsets.symmetric(horizontal: 8), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( + ...showItems.map((item) => IntrinsicWidth( + child: Container( + height: 28, + padding: const EdgeInsets.symmetric(horizontal: 10), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark, + width: 0.7), + ), + child: Text( + item, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w500, color: AppColors.primaryDark, - width: 0.7), - ), - child: Text( - item, - style: const TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.primaryDark, + ), ), ), )), if (hasMore && !_expanded) GestureDetector( onTap: () => setState(() => _expanded = true), - child: Container( - height: 28, - padding: const EdgeInsets.symmetric(horizontal: 8), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppColors.primaryDark.withValues(alpha: 0.15), - width: 1), - ), - child: Text( - '+${widget.items.length - widget.initialCount} More', - style: const TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 15, - fontWeight: FontWeight.w300, - color: AppColors.primaryDark, + child: IntrinsicWidth( + child: Container( + height: 28, + padding: const EdgeInsets.symmetric(horizontal: 10), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.15), + width: 1), + ), + child: Text( + '+${widget.items.length - widget.initialCount} More', + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 15, + fontWeight: FontWeight.w300, + color: AppColors.primaryDark, + ), ), ), ), @@ -1230,7 +1236,7 @@ class _SpecializationCardWidget extends StatefulWidget { class _SpecializationCardWidgetState extends State<_SpecializationCardWidget> { bool _expanded = false; - static const _initialCount = 5; + static const _initialCount = 3; IconData get _icon { final slug = widget.card.slug.toLowerCase(); diff --git a/lib/features/agents/presentation/screens/agent_search_screen.dart b/lib/features/agents/presentation/screens/agent_search_screen.dart index 6620d14..081377d 100644 --- a/lib/features/agents/presentation/screens/agent_search_screen.dart +++ b/lib/features/agents/presentation/screens/agent_search_screen.dart @@ -414,6 +414,7 @@ class _AgentSearchScreenState extends ConsumerState { context.push('/login'); return; } + context.push('/profile'); }, ), ], diff --git a/lib/features/contact/presentation/screens/contact_screen.dart b/lib/features/contact/presentation/screens/contact_screen.dart new file mode 100644 index 0000000..3dc294a --- /dev/null +++ b/lib/features/contact/presentation/screens/contact_screen.dart @@ -0,0 +1,691 @@ +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 { + const ContactScreen({super.key}); + + @override + ConsumerState createState() => _ContactScreenState(); +} + +class _ContactScreenState extends ConsumerState { + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + final _phoneController = TextEditingController(); + final _messageController = TextEditingController(); + bool _isSending = false; + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _messageController.dispose(); + super.dispose(); + } + + @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(), + ); + } + + // -- Title -- + Widget _buildTitle() { + return const Center( + child: Text( + 'Get In Touch', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 25, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + ); + } + + // -- Description -- + Widget _buildDescription() { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + 'Have a question about a property or need assistance? Fill out the form below and our team will get back to you shortly.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ); + } + + // -- Contact Form -- + Widget _buildContactForm() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 23), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabel('Your Name'), + const SizedBox(height: 8), + _buildTextField( + controller: _nameController, + hintText: 'Brain Neeland', + ), + const SizedBox(height: 24), + _buildLabel('Email Address'), + const SizedBox(height: 8), + _buildTextField( + controller: _emailController, + hintText: '123@gmail.com', + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email, + ), + const SizedBox(height: 24), + _buildLabel('Phone Number'), + const SizedBox(height: 8), + _buildTextField( + controller: _phoneController, + hintText: '12345678990', + keyboardType: TextInputType.phone, + prefixIcon: Icons.call, + ), + const SizedBox(height: 24), + _buildLabel('Message'), + const SizedBox(height: 8), + _buildMessageField(), + const SizedBox(height: 30), + _buildFormButtons(), + ], + ), + ); + } + + Widget _buildLabel(String text) { + return Text( + text, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String hintText, + TextInputType? keyboardType, + IconData? prefixIcon, + }) { + final borderSide = BorderSide( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ); + final border = OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: borderSide, + ); + + return SizedBox( + height: 52, + child: TextField( + controller: controller, + keyboardType: keyboardType, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w200, + color: Colors.black, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + hintText: hintText, + hintStyle: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w200, + color: Colors.black.withValues(alpha: 0.4), + ), + border: border, + enabledBorder: border, + focusedBorder: border, + disabledBorder: border, + prefixIcon: prefixIcon != null + ? Icon(prefixIcon, size: 22, color: AppColors.primaryDark) + : null, + prefixIconConstraints: prefixIcon != null + ? const BoxConstraints(minWidth: 48) + : null, + ), + ), + ); + } + + Widget _buildMessageField() { + final borderSide = BorderSide( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ); + final border = OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: borderSide, + ); + + return SizedBox( + height: 183, + child: TextField( + controller: _messageController, + maxLines: null, + expands: true, + textAlignVertical: TextAlignVertical.top, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w200, + color: Colors.black, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + hintText: 'Write your message here...', + hintStyle: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w200, + color: Colors.black.withValues(alpha: 0.4), + ), + border: border, + enabledBorder: border, + focusedBorder: border, + disabledBorder: border, + ), + ), + ); + } + + Widget _buildFormButtons() { + return Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => context.pop(), + child: Container( + height: 46, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ), + ), + alignment: Alignment.center, + child: const Text( + 'Cancel', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + ), + ), + ), + const SizedBox(width: 23), + Expanded( + child: GestureDetector( + onTap: _isSending ? null : _sendMessage, + child: Container( + height: 46, + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + child: _isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primaryDark, + ), + ) + : const Text( + 'Send Message', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + ), + ), + ), + ], + ); + } + + // -- Contact Details Section -- + Widget _buildContactDetails() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Column( + children: [ + const SizedBox(height: 17), + // Title + const Text( + 'Contact Details', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 12), + _buildDetailDivider(), + // Email Support + _buildContactDetailRow( + icon: Icons.email_outlined, + label: 'Email Support', + value: '123support@gmail.com', + ), + _buildDetailDivider(), + // Phone + _buildContactDetailRow( + icon: Icons.call_outlined, + label: 'Phone', + value: '1234567890', + subtitle: 'Mon-Fri 9am-6pm', + ), + _buildDetailDivider(), + // Office + _buildContactDetailRow( + icon: Icons.location_on_outlined, + label: 'Office', + value: '123 Market Street\nNew York CA 234737', + ), + const SizedBox(height: 16), + // Map placeholder + Padding( + padding: const EdgeInsets.symmetric(horizontal: 36), + child: Container( + height: 166, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ), + color: const Color(0xFFF5F5F5), + ), + alignment: Alignment.center, + child: Icon( + Icons.map_outlined, + size: 48, + color: AppColors.primaryDark.withValues(alpha: 0.3), + ), + ), + ), + const SizedBox(height: 16), + // Get Directions button + GestureDetector( + onTap: () {}, + child: Container( + width: 120, + height: 35, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.accentOrange, + width: 1, + ), + ), + alignment: Alignment.center, + child: const Text( + 'Get Directions', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.accentOrange, + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _buildDetailDivider() { + return Divider( + color: AppColors.primaryDark.withValues(alpha: 0.1), + height: 1, + thickness: 1, + indent: 1, + endIndent: 1, + ); + } + + Widget _buildContactDetailRow({ + required IconData icon, + required String label, + required String value, + String? subtitle, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + size: 22, + color: AppColors.primaryDark, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w300, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 10, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ], + ), + ), + ], + ), + ); + } + + // -- Find Agent Section -- + Widget _buildFindAgentSection() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + children: [ + const Text( + 'Ready to find an agent?', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 25, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 16), + const Text( + 'Discover trusted agents and start your property journey with confidence.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 24), + // Illustration placeholder + Container( + width: 254, + height: 254, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFFFF5EB), + ), + alignment: Alignment.center, + child: Icon( + Icons.real_estate_agent, + size: 80, + color: AppColors.accentOrange.withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 24), + // Start Your Search button + GestureDetector( + onTap: () => context.push('/agents/search'), + child: Container( + height: 46, + width: 200, + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Start Your Search', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + size: 18, + color: AppColors.primaryDark, + ), + ], + ), + ), + ), + ], + ), + ); + } + + // -- Send Message -- + void _sendMessage() { + final name = _nameController.text.trim(); + final email = _emailController.text.trim(); + final message = _messageController.text.trim(); + + if (name.isEmpty || email.isEmpty || message.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please fill in all required fields'), + backgroundColor: Colors.red, + ), + ); + return; + } + + setState(() => _isSending = true); + + // Simulate sending — replace with actual API call + Future.delayed(const Duration(seconds: 2), () { + if (!mounted) return; + setState(() => _isSending = false); + _nameController.clear(); + _emailController.clear(); + _phoneController.clear(); + _messageController.clear(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Message sent successfully!'), + backgroundColor: Color(0xFF638559), + ), + ); + }); + } + + // -- 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 5e07567..7b1d2a9 100644 --- a/lib/features/faq/presentation/screens/faq_screen.dart +++ b/lib/features/faq/presentation/screens/faq_screen.dart @@ -275,6 +275,7 @@ class FaqScreen extends ConsumerWidget { context.push('/login'); return; } + context.go('/messages'); }, ), _buildNavItem( @@ -288,6 +289,7 @@ class FaqScreen extends ConsumerWidget { context.push('/login'); return; } + context.push('/profile'); }, ), ], diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart index 9d43860..f599196 100644 --- a/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -165,6 +165,8 @@ class _HomeScreenState extends ConsumerState { context.push('/login'); return; } + context.push('/profile'); + return; } setState(() => _selectedIndex = index); }, diff --git a/lib/features/home/presentation/widgets/home_header.dart b/lib/features/home/presentation/widgets/home_header.dart index 7cfabb4..10d871f 100644 --- a/lib/features/home/presentation/widgets/home_header.dart +++ b/lib/features/home/presentation/widgets/home_header.dart @@ -163,7 +163,10 @@ class _MenuDrawer extends ConsumerWidget { context, icon: Icons.mail_outline, label: 'Contact', - onTap: () => Navigator.pop(context), + onTap: () { + Navigator.pop(context); + context.push('/contact'); + }, ), const SizedBox(height: 16), const Divider(color: AppColors.divider, height: 1), @@ -174,13 +177,19 @@ class _MenuDrawer extends ConsumerWidget { context, icon: Icons.person_outline, label: 'Account Settings', - onTap: () => Navigator.pop(context), + onTap: () { + Navigator.pop(context); + context.push('/profile'); + }, ), _buildMenuItem( context, icon: Icons.notifications_outlined, label: 'Notification Settings', - onTap: () => Navigator.pop(context), + onTap: () { + Navigator.pop(context); + context.push('/profile'); + }, ), const SizedBox(height: 16), const Divider(color: AppColors.divider, height: 1), diff --git a/lib/features/messaging/presentation/screens/chat_screen.dart b/lib/features/messaging/presentation/screens/chat_screen.dart index a92de00..dc25a82 100644 --- a/lib/features/messaging/presentation/screens/chat_screen.dart +++ b/lib/features/messaging/presentation/screens/chat_screen.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/config/app_config.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; @@ -91,7 +92,7 @@ class _ChatScreenState extends ConsumerState { void _handleBack() { ref.read(messagingProvider.notifier).setActiveConversation(null); - Navigator.pop(context); + context.go('/messages'); } // ============================================================ @@ -183,51 +184,37 @@ class _ChatScreenState extends ConsumerState { final otherPartyName = conversation?.otherParty.name ?? 'Chat'; final otherPartyAvatar = conversation?.otherParty.avatar; final isOnline = conversation?.otherParty.isOnline ?? false; - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Column( - children: [ - _buildHeader( - name: otherPartyName, - isOnline: isOnline, - ), - Expanded( - child: messagingState.isLoadingMessages && messages.isEmpty - ? const Center( - child: CircularProgressIndicator( - color: AppColors.accentOrange, - strokeWidth: 2, - ), - ) - : messages.isEmpty - ? Center( - child: Text( - 'No messages yet.\nSay hello!', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 14, - fontWeight: FontWeight.w400, - color: AppColors.hintText, - ), - ), - ) - : _buildMessagesList( - messages: messages, - currentUserId: currentUserId ?? '', - currentUserName: 'You', - otherPartyName: otherPartyName, - otherPartyAvatar: otherPartyAvatar, - isTyping: isTyping, - isLoadingMore: messagingState.isLoadingMessages && - messages.isNotEmpty, - ), - ), - _buildInputArea(), - ], + final otherPartyHeadline = conversation?.otherParty.headline; + + return Column( + children: [ + // Chat header with name + status + _buildHeader( + name: otherPartyName, + isOnline: isOnline, ), - ), + Expanded( + child: messagingState.isLoadingMessages && messages.isEmpty + ? const Center( + child: CircularProgressIndicator( + color: AppColors.accentOrange, + strokeWidth: 2, + ), + ) + : _buildChatBody( + messages: messages, + currentUserId: currentUserId ?? '', + otherPartyName: otherPartyName, + otherPartyAvatar: otherPartyAvatar, + otherPartyHeadline: otherPartyHeadline, + isOnline: isOnline, + isTyping: isTyping, + isLoadingMore: messagingState.isLoadingMessages && + messages.isNotEmpty, + ), + ), + _buildInputArea(), + ], ); } @@ -240,104 +227,277 @@ class _ChatScreenState extends ConsumerState { required bool isOnline, }) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Container( - height: 55, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.primaryDark, - width: 0.5, - ), - borderRadius: BorderRadius.circular(7), - ), - child: Row( - children: [ - // Back arrow - GestureDetector( - onTap: _handleBack, - behavior: HitTestBehavior.opaque, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 14), - child: Icon( - Icons.arrow_back, - size: 23, - color: AppColors.primaryDark, - ), + padding: const EdgeInsets.fromLTRB(12, 10, 12, 6), + child: Row( + children: [ + // Back arrow + GestureDetector( + onTap: _handleBack, + behavior: HitTestBehavior.opaque, + child: const Padding( + padding: EdgeInsets.only(right: 16), + child: Icon( + Icons.arrow_back, + size: 22, + color: AppColors.primaryDark, ), ), - // Name and active status - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.primaryDark, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ), + // Name and active status + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, ), - const SizedBox(height: 2), - Row( - children: [ - if (isOnline) ...[ - Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Color(0xFF4CAF50), - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 5), - ], - Text( - isOnline ? 'Active Now' : 'Offline', - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 10, - fontWeight: FontWeight.w400, - color: AppColors.primaryDark, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Row( + children: [ + if (isOnline) ...[ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFF4CAF50), + shape: BoxShape.circle, ), ), + const SizedBox(width: 5), ], - ), - ], + Text( + isOnline ? 'Active Now' : 'Offline', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 10, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ], + ), + ), + // Three dots (vertical) + GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Icon( + Icons.more_vert, + size: 20, + color: AppColors.primaryDark, ), ), - // Three dots (vertical) - GestureDetector( - onTap: () {}, - behavior: HitTestBehavior.opaque, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Icon( - Icons.more_vert, - size: 20, - color: AppColors.primaryDark, - ), + ), + // Star icon + GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.star_rounded, + size: 24, + color: AppColors.accentOrange, ), ), - // Star icon - GestureDetector( - onTap: () {}, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.only(right: 14), - child: Icon( - Icons.star_rounded, - size: 24, - color: AppColors.accentOrange, - ), + ), + ], + ), + ); + } + + // ============================================================ + // CHAT BODY (profile section + messages) + // ============================================================ + + Widget _buildChatBody({ + required List messages, + required String currentUserId, + required String otherPartyName, + String? otherPartyAvatar, + String? otherPartyHeadline, + required bool isOnline, + required bool isTyping, + required bool isLoadingMore, + }) { + if (messages.isEmpty) { + return SingleChildScrollView( + child: Column( + children: [ + _buildProfileSection( + name: otherPartyName, + avatar: otherPartyAvatar, + headline: otherPartyHeadline, + isOnline: isOnline, + ), + const SizedBox(height: 60), + Text( + 'No messages yet.\nSay hello!', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.hintText, ), ), ], ), + ); + } + + return _buildMessagesList( + messages: messages, + currentUserId: currentUserId, + currentUserName: 'You', + otherPartyName: otherPartyName, + otherPartyAvatar: otherPartyAvatar, + otherPartyHeadline: otherPartyHeadline, + isOnline: isOnline, + isTyping: isTyping, + isLoadingMore: isLoadingMore, + ); + } + + // ============================================================ + // PROFILE SECTION (large avatar, name, specialties) + // ============================================================ + + Widget _buildProfileSection({ + required String name, + String? avatar, + String? headline, + required bool isOnline, + }) { + final avatarUrl = _resolveAvatarUrl(avatar); + final initials = _getInitials(name); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Large avatar with online dot + Stack( + children: [ + ClipOval( + child: avatarUrl.isNotEmpty + ? CachedNetworkImage( + imageUrl: avatarUrl, + width: 80, + height: 80, + fit: BoxFit.cover, + errorWidget: (_, __, ___) => + _AvatarFallback(letter: initials, size: 80), + ) + : _AvatarFallback(letter: initials, size: 80), + ), + if (isOnline) + Positioned( + right: 2, + bottom: 2, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: const Color(0xFF4CAF50), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2.5), + ), + ), + ), + ], + ), + const SizedBox(height: 14), + // Name with pronouns and orange dot + Row( + children: [ + Text( + name, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 4), + const Text( + '(He/Him)', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 6), + Container( + width: 10, + height: 10, + decoration: const BoxDecoration( + color: AppColors.accentOrange, + shape: BoxShape.circle, + ), + ), + ], + ), + const SizedBox(height: 12), + // Specialties row + if (headline != null && headline.isNotEmpty) + _buildSpecialtiesRow(headline) + else + _buildSpecialtiesRow('Sales,Analytics,Inspection,Residential,Commercial'), + const SizedBox(height: 12), + // Divider + Divider( + color: AppColors.primaryDark.withValues(alpha: 0.1), + thickness: 0.5, + ), + ], + ), + ); + } + + Widget _buildSpecialtiesRow(String headline) { + // Split by comma, pipe, or similar separators + final specialties = headline.split(RegExp(r'[,|;]')) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + if (specialties.isEmpty) return const SizedBox.shrink(); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (int i = 0; i < specialties.length; i++) ...[ + Text( + specialties[i], + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + if (i < specialties.length - 1) + const SizedBox(width: 16), + ], + ], ), ); } @@ -352,16 +512,19 @@ class _ChatScreenState extends ConsumerState { required String currentUserName, required String otherPartyName, String? otherPartyAvatar, + String? otherPartyHeadline, + required bool isOnline, required bool isTyping, required bool isLoadingMore, }) { + // +1 for the profile section header at the end of reversed list final itemCount = - messages.length + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0); + messages.length + 1 + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0); return ListView.builder( controller: _scrollController, reverse: true, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), itemCount: itemCount, itemBuilder: (context, index) { if (isTyping && index == 0) { @@ -370,6 +533,17 @@ class _ChatScreenState extends ConsumerState { final adjustedIndex = isTyping ? index - 1 : index; + // Profile section at the top (last index in reversed list) + final profileIndex = messages.length + (isLoadingMore ? 1 : 0); + if (adjustedIndex == profileIndex) { + return _buildProfileSection( + name: otherPartyName, + avatar: otherPartyAvatar, + headline: otherPartyHeadline, + isOnline: isOnline, + ); + } + if (isLoadingMore && adjustedIndex == messages.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), @@ -404,19 +578,22 @@ class _ChatScreenState extends ConsumerState { : otherPartyName; final senderAvatar = isMine ? null : otherPartyAvatar; - return Column( - children: [ - if (showDateSeparator) - _buildDateSeparator( - DateTime.parse(message.createdAt).toLocal(), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + if (showDateSeparator) + _buildDateSeparator( + DateTime.parse(message.createdAt).toLocal(), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: isMine + ? _buildSentMessage(message, senderName) + : _buildReceivedMessage(message, senderName, senderAvatar), ), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: isMine - ? _buildSentMessage(message, senderName) - : _buildReceivedMessage(message, senderName, senderAvatar), - ), - ], + ], + ), ); }, ); @@ -495,6 +672,16 @@ class _ChatScreenState extends ConsumerState { ), ), const SizedBox(width: 6), + const Text( + 'He/Him', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 10, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 6), Container( width: 8, height: 8, @@ -567,6 +754,16 @@ class _ChatScreenState extends ConsumerState { ), ), const SizedBox(width: 6), + const Text( + 'He/Him', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 10, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 6), Text( timestamp, style: const TextStyle( @@ -667,41 +864,30 @@ class _ChatScreenState extends ConsumerState { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Plus icon in circle border + // Plus icon (plain, no border) GestureDetector( onTap: _showAttachmentOptions, - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - border: Border.all( - color: AppColors.primaryDark, - width: 0.5, - ), - ), - child: const Center( - child: Icon( - Icons.add, - size: 24, - color: AppColors.primaryDark, - ), + behavior: HitTestBehavior.opaque, + child: const Padding( + padding: EdgeInsets.only(right: 10), + child: Icon( + Icons.add, + size: 24, + color: AppColors.primaryDark, ), ), ), - const SizedBox(width: 10), - // Text input + // Text input pill with send icon inside Expanded( child: Container( - height: 38, + height: 40, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: AppColors.primaryDark, - width: 0.5, + width: 1.5, ), ), child: Row( @@ -736,7 +922,7 @@ class _ChatScreenState extends ConsumerState { ), ), ), - // Send icon inside the input field + // Send arrow inside the pill GestureDetector( onTap: _sendMessage, behavior: HitTestBehavior.opaque, @@ -755,25 +941,21 @@ class _ChatScreenState extends ConsumerState { ), const SizedBox(width: 10), - // Mic icon in circle border + // Mic icon in filled dark circle GestureDetector( onTap: () => _showComingSoon('Speech to text'), child: Container( width: 36, height: 36, - decoration: BoxDecoration( - color: Colors.white, + decoration: const BoxDecoration( + color: AppColors.primaryDark, shape: BoxShape.circle, - border: Border.all( - color: AppColors.primaryDark, - width: 0.5, - ), ), child: const Center( child: Icon( Icons.mic_none, size: 20, - color: AppColors.primaryDark, + color: Colors.white, ), ), ), @@ -873,6 +1055,7 @@ class _ChatScreenState extends ConsumerState { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ); } + } // -- Avatar Fallback -- diff --git a/lib/features/messaging/presentation/screens/conversations_screen.dart b/lib/features/messaging/presentation/screens/conversations_screen.dart index da24b8d..ac61335 100644 --- a/lib/features/messaging/presentation/screens/conversations_screen.dart +++ b/lib/features/messaging/presentation/screens/conversations_screen.dart @@ -52,16 +52,11 @@ class _ConversationsScreenState extends ConsumerState { final state = ref.watch(messagingProvider); final filtered = _filteredConversations(state.conversations); - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Column( - children: [ - _buildHeader(), - Expanded(child: _buildConversationList(state, filtered)), - ], - ), - ), + return Column( + children: [ + _buildHeader(), + Expanded(child: _buildConversationList(state, filtered)), + ], ); } @@ -317,12 +312,13 @@ class _ConversationsScreenState extends ConsumerState { return _ConversationTile( conversation: filtered[index], onTap: () => - context.push('/messages/chat/${filtered[index].id}'), + context.go('/messages/chat/${filtered[index].id}'), ); }, ), ); } + } // -- Conversation Tile Widget -- diff --git a/lib/features/messaging/presentation/screens/messaging_shell.dart b/lib/features/messaging/presentation/screens/messaging_shell.dart new file mode 100644 index 0000000..35d6493 --- /dev/null +++ b/lib/features/messaging/presentation/screens/messaging_shell.dart @@ -0,0 +1,117 @@ +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 MessagingShell extends ConsumerWidget { + final Widget child; + + const MessagingShell({super.key, required this.child}); + + @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), + ); + } + + 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: true, + onTap: () => 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, + ), + ), + ), + ); + } +} diff --git a/lib/features/profile/data/profile_repository.dart b/lib/features/profile/data/profile_repository.dart new file mode 100644 index 0000000..697bcb4 --- /dev/null +++ b/lib/features/profile/data/profile_repository.dart @@ -0,0 +1,66 @@ +import 'package:dio/dio.dart'; +import 'package:real_estate_mobile/core/network/api_client.dart'; +import 'package:real_estate_mobile/core/network/api_exceptions.dart'; + +class ProfileRepository { + final Dio _dio = ApiClient.instance.dio; + + /// Fetch profile based on role. + /// Agent: GET /agents/profile/me + /// User: GET /users/profile/me + Future> getMyProfile(String role) async { + try { + final endpoint = role == 'AGENT' + ? '/agents/profile/me' + : '/users/profile/me'; + final response = await _dio.get(endpoint); + return response.data['data'] as Map; + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException( + message: e.message ?? 'Failed to fetch profile', + statusCode: e.response?.statusCode, + ); + } + } + + /// Update profile based on role. + /// Agent: PUT /agents/profile + /// User: PATCH /users/profile/me + Future> updateProfile( + String role, + Map data, + ) async { + try { + final Response response; + if (role == 'AGENT') { + response = await _dio.put('/agents/profile', data: data); + } else { + response = await _dio.patch('/users/profile/me', data: data); + } + return response.data['data'] as Map; + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException( + message: e.message ?? 'Failed to update profile', + statusCode: e.response?.statusCode, + ); + } + } + + Future getAvatarUploadUrl(String role, String fileName) async { + try { + final endpoint = role == 'AGENT' + ? '/upload/avatar-presigned-url' + : '/upload/user-avatar-presigned-url'; + final response = await _dio.post(endpoint, data: {'fileName': fileName}); + return response.data['data']['url'] as String; + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException( + message: e.message ?? 'Failed to get upload URL', + statusCode: e.response?.statusCode, + ); + } + } +} diff --git a/lib/features/profile/presentation/providers/profile_provider.dart b/lib/features/profile/presentation/providers/profile_provider.dart new file mode 100644 index 0000000..64a10eb --- /dev/null +++ b/lib/features/profile/presentation/providers/profile_provider.dart @@ -0,0 +1,98 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; +import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; + +// Profile state +class ProfileState { + final String role; + final Map profile; + final bool isLoading; + final bool isSaving; + final String? errorMessage; + final String? successMessage; + + const ProfileState({ + this.role = 'USER', + this.profile = const {}, + this.isLoading = true, + this.isSaving = false, + this.errorMessage, + this.successMessage, + }); + + bool get isAgent => role == 'AGENT'; + + ProfileState copyWith({ + String? role, + Map? profile, + bool? isLoading, + bool? isSaving, + String? errorMessage, + String? successMessage, + bool clearError = false, + bool clearSuccess = false, + }) { + return ProfileState( + role: role ?? this.role, + profile: profile ?? this.profile, + isLoading: isLoading ?? this.isLoading, + isSaving: isSaving ?? this.isSaving, + errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), + successMessage: + clearSuccess ? null : (successMessage ?? this.successMessage), + ); + } +} + +class ProfileNotifier extends StateNotifier { + final ProfileRepository _repository; + final String _role; + + ProfileNotifier(this._repository, this._role) + : super(ProfileState(role: _role)) { + loadProfile(); + } + + Future loadProfile() async { + state = state.copyWith(isLoading: true, clearError: true); + try { + final profile = await _repository.getMyProfile(_role); + state = state.copyWith(profile: profile, isLoading: false); + } catch (e) { + state = state.copyWith( + isLoading: false, + errorMessage: e.toString(), + ); + } + } + + Future updateProfile(Map data) async { + state = + state.copyWith(isSaving: true, clearError: true, clearSuccess: true); + try { + final updated = await _repository.updateProfile(_role, data); + state = state.copyWith( + profile: updated, + isSaving: false, + successMessage: 'Profile updated successfully', + ); + } catch (e) { + state = state.copyWith( + isSaving: false, + errorMessage: 'Failed to update profile', + ); + } + } +} + +// Providers +final profileRepositoryProvider = Provider((ref) { + return ProfileRepository(); +}); + +final profileProvider = + StateNotifierProvider.autoDispose((ref) { + final authState = ref.watch(authProvider); + final role = authState.user?.role ?? 'USER'; + return ProfileNotifier(ref.watch(profileRepositoryProvider), role); +}); diff --git a/lib/features/profile/presentation/screens/profile_settings_screen.dart b/lib/features/profile/presentation/screens/profile_settings_screen.dart new file mode 100644 index 0000000..f33a37d --- /dev/null +++ b/lib/features/profile/presentation/screens/profile_settings_screen.dart @@ -0,0 +1,1069 @@ +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 { + const ProfileSettingsScreen({super.key}); + + @override + ConsumerState createState() => + _ProfileSettingsScreenState(); +} + +class _ProfileSettingsScreenState extends ConsumerState { + int _selectedTab = 0; + + // Profile form controllers + late TextEditingController _fullNameController; + late TextEditingController _titleController; + late TextEditingController _emailController; + late TextEditingController _phoneController; + late TextEditingController _locationController; + + // Notification preferences + final Map> _notificationPrefs = { + 'newLeadAlerts': {'email': true, 'inApp': true}, + 'messageUpdates': {'email': true, 'inApp': true}, + 'urgentRequests': {'email': true, 'inApp': true}, + }; + String _digestFrequency = 'instant'; + + bool _controllersInitialized = false; + + @override + void initState() { + super.initState(); + _fullNameController = TextEditingController(); + _titleController = TextEditingController(); + _emailController = TextEditingController(); + _phoneController = TextEditingController(); + _locationController = TextEditingController(); + } + + @override + void dispose() { + _fullNameController.dispose(); + _titleController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _locationController.dispose(); + super.dispose(); + } + + void _initControllersFromProfile(ProfileState profileState) { + if (_controllersInitialized) return; + _controllersInitialized = true; + + final profile = profileState.profile; + final isAgent = profileState.isAgent; + + final firstName = profile['firstName'] as String? ?? ''; + final lastName = profile['lastName'] as String? ?? ''; + _fullNameController.text = '$firstName $lastName'.trim(); + _emailController.text = profile['email'] as String? ?? + (profile['user'] is Map + ? (profile['user'] as Map)['email'] as String? ?? '' + : ''); + _phoneController.text = profile['phone'] as String? ?? ''; + + if (isAgent) { + // Agent: headline from agentType.name or bio + final agentType = profile['agentType'] as Map?; + _titleController.text = + agentType?['name'] as String? ?? profile['bio'] as String? ?? ''; + // Agent: serviceAreas for location + final serviceAreas = profile['serviceAreas']; + if (serviceAreas is List && serviceAreas.isNotEmpty) { + _locationController.text = serviceAreas.first.toString(); + } + } else { + // User: headline + _titleController.text = profile['headline'] as String? ?? ''; + // User: city/state/country + final city = profile['city'] as String? ?? ''; + final stateName = profile['state'] as String? ?? ''; + final country = profile['country'] as String? ?? ''; + final parts = [city, stateName, country].where((s) => s.isNotEmpty); + _locationController.text = parts.join(', '); + } + } + + @override + Widget build(BuildContext context) { + final profileState = ref.watch(profileProvider); + + if (!profileState.isLoading && profileState.profile.isNotEmpty) { + _initControllersFromProfile(profileState); + } + + // Show success/error snackbars + ref.listen(profileProvider, (prev, next) { + if (next.successMessage != null && prev?.successMessage == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(next.successMessage!), + backgroundColor: const Color(0xFF638559), + ), + ); + } + if (next.errorMessage != null && prev?.errorMessage == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(next.errorMessage!), + backgroundColor: Colors.red, + ), + ); + } + }); + + 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(), + ); + } + + // ── Tab Bar ── + + Widget _buildTabBar() { + return SizedBox( + height: 46, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 7), + children: [ + _buildTab(0, 'Profile Settings', Icons.person), + const SizedBox(width: 6), + _buildTab(1, 'Notifications', Icons.notifications), + const SizedBox(width: 6), + _buildTab(2, 'Privacy', Icons.person), + ], + ), + ); + } + + Widget _buildTab(int index, String label, IconData icon) { + final isActive = _selectedTab == index; + return GestureDetector( + onTap: () => setState(() => _selectedTab = index), + child: Container( + width: 162, + height: 46, + decoration: BoxDecoration( + color: isActive ? AppColors.accentOrange : Colors.transparent, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: isActive + ? AppColors.accentOrange + : AppColors.primaryDark.withValues(alpha: 0.2), + width: 0.1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 24, color: AppColors.primaryDark), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + ); + } + + Widget _buildProgressBar() { + final progress = (_selectedTab + 1) / 3; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 16), + child: Container( + height: 5, + decoration: BoxDecoration( + color: const Color(0xFFD9D9D9), + borderRadius: BorderRadius.circular(15), + ), + child: Align( + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: progress, + child: Container( + decoration: BoxDecoration( + color: AppColors.primaryDark.withValues(alpha: 0.75), + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + ), + ); + } + + // ── Profile Settings Tab ── + + Widget _buildProfileSettingsTab(ProfileState profileState) { + final profile = profileState.profile; + final isAgent = profileState.isAgent; + + // Get avatar URL — agent avatar might be an S3 key or full URL + String? avatarUrl = profile['avatar'] as String?; + if (avatarUrl == null || avatarUrl.isEmpty) { + // Try nested user object (agent profile structure) + final user = profile['user'] as Map?; + avatarUrl = user?['avatar'] as String?; + } + + final descriptionText = isAgent + ? 'This information will be displayed on your public agent page.' + : 'This information will be displayed on your profile.'; + + return ListView( + padding: const EdgeInsets.fromLTRB(31, 10, 31, 30), + children: [ + // Section title + const Text( + 'Public Profile', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + Text( + descriptionText, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 39), + + // Avatar + Center( + child: Column( + children: [ + Container( + width: 71, + height: 69, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppColors.accentOrange, + width: 2, + ), + ), + child: ClipOval( + child: avatarUrl != null && avatarUrl.isNotEmpty + ? Image.network( + avatarUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Icon( + Icons.person, + size: 36, + color: AppColors.primaryDark, + ), + ) + : const Icon( + Icons.person, + size: 36, + color: AppColors.primaryDark, + ), + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: _pickAvatar, + child: Container( + width: 79, + height: 24, + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(7), + ), + alignment: Alignment.center, + child: const Text( + 'Upload Now', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 11, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + const SizedBox(width: 6), + GestureDetector( + onTap: () { + ref + .read(profileProvider.notifier) + .updateProfile({'avatar': ''}); + }, + child: Container( + width: 79, + height: 24, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + border: Border.all( + color: AppColors.primaryDark, + width: 0.1, + ), + ), + alignment: Alignment.center, + child: const Text( + 'Delete', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 11, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 49), + + // Form fields + _buildFormField('Full Name', _fullNameController), + const SizedBox(height: 24), + _buildFormField( + isAgent ? 'Career / Agent Type' : 'Professional Title', + _titleController, + enabled: !isAgent, // Agent type is read-only + ), + const SizedBox(height: 24), + _buildFormField('Email Address', _emailController, enabled: false), + const SizedBox(height: 24), + _buildFormField('Phone Number', _phoneController, + keyboardType: TextInputType.phone), + const SizedBox(height: 24), + _buildFormField( + isAgent ? 'Service Areas' : 'Location', + _locationController, + prefixIcon: Icons.location_on, + ), + const SizedBox(height: 30), + + // Action buttons + _buildActionButtons(onSave: _saveProfileSettings), + ], + ); + } + + Widget _buildFormField( + String label, + TextEditingController controller, { + bool enabled = true, + TextInputType? keyboardType, + IconData? prefixIcon, + }) { + final borderSide = BorderSide( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ); + final border = OutlineInputBorder( + borderRadius: BorderRadius.circular(7), + borderSide: borderSide, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 14), + SizedBox( + height: 38, + child: TextField( + controller: controller, + enabled: enabled, + keyboardType: keyboardType, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 26, + vertical: 8, + ), + border: border, + enabledBorder: border, + focusedBorder: border, + disabledBorder: border, + prefixIcon: prefixIcon != null + ? Icon(prefixIcon, size: 18, color: AppColors.accentOrange) + : null, + prefixIconConstraints: prefixIcon != null + ? const BoxConstraints(minWidth: 40) + : null, + ), + ), + ), + ], + ); + } + + void _pickAvatar() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Avatar upload coming soon')), + ); + } + + void _saveProfileSettings() { + final profileState = ref.read(profileProvider); + final isAgent = profileState.isAgent; + + final nameParts = _fullNameController.text.trim().split(RegExp(r'\s+')); + final firstName = nameParts.first; + final lastName = + nameParts.length > 1 ? nameParts.sublist(1).join(' ') : ''; + + final data = { + 'firstName': firstName, + 'lastName': lastName, + 'phone': _phoneController.text.trim(), + }; + + if (isAgent) { + // Agent: serviceAreas as array + final location = _locationController.text.trim(); + if (location.isNotEmpty) { + data['serviceAreas'] = [location]; + } + } else { + // User: city/state/country + headline + data['headline'] = _titleController.text.trim(); + final locationParts = _locationController.text + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + if (locationParts.isNotEmpty) data['city'] = locationParts[0]; + if (locationParts.length > 1) data['state'] = locationParts[1]; + if (locationParts.length > 2) data['country'] = locationParts[2]; + } + + ref.read(profileProvider.notifier).updateProfile(data); + } + + // ── Notifications Tab ── + + Widget _buildNotificationsTab() { + return ListView( + padding: const EdgeInsets.fromLTRB(27, 10, 27, 30), + children: [ + // Title — Figma: Fractul Bold 15px, y=263 + const Text( + 'Notification Preferences', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + // Description — Figma: SourceSerif4 Regular 14px, y=291 + const Text( + 'Choose what you want to be notified about and how.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 20), + + // Divider — Figma: y=322 + _buildSectionDivider(), + const SizedBox(height: 19), + + // New Lead Alerts — Figma: title y=341 + _buildNotificationCategory( + title: 'New Lead Alerts', + description: 'Choose what you want to be notified about and how.', + prefKey: 'newLeadAlerts', + ), + + // Divider — Figma: y=416 + _buildSectionDivider(), + const SizedBox(height: 13), + + // Message Updates — Figma: title y=429 + _buildNotificationCategory( + title: 'Message Updates', + description: + 'Notices about new messages from clients or other agents.', + prefKey: 'messageUpdates', + ), + + // Divider — Figma: y=511 + _buildSectionDivider(), + const SizedBox(height: 16), + + // Urgent Requests & Tours — Figma: title y=527 + _buildNotificationCategory( + title: 'Urgent Requests & Tours', + description: + 'Immediate notifications for tour requests and time-sensitive items.', + prefKey: 'urgentRequests', + ), + + // Divider — Figma: y=621 + _buildSectionDivider(), + const SizedBox(height: 20), + + // Email Digest Frequency — Figma: Fractul Bold 14px, y=641 + const Text( + 'Email Digest Frequency', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + // Description — Figma: y=667 + const Text( + 'Choose how often you receive non-urgent email summaries', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 16), + + // Frequency selector — Figma: y=731 + _buildFrequencySelector(), + const SizedBox(height: 40), + + // Action buttons — Figma: y=879, rounded-[15px] + _buildNotificationActionButtons(), + ], + ); + } + + Widget _buildSectionDivider() { + return Divider( + color: AppColors.primaryDark.withValues(alpha: 0.15), + height: 1, + thickness: 1, + ); + } + + Widget _buildNotificationCategory({ + required String title, + required String description, + required String prefKey, + }) { + final prefs = _notificationPrefs[prefKey]!; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left: title + description (max width ~220 from Figma) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + const SizedBox(width: 20), + // Right: checkbox labels — Figma: x=321, Email/In-App stacked ~26px apart + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCheckboxRow( + label: 'Email', + value: prefs['email']!, + onChanged: (val) { + setState(() { + _notificationPrefs[prefKey]!['email'] = val ?? false; + }); + }, + ), + const SizedBox(height: 6), + _buildCheckboxRow( + label: 'In-App', + value: prefs['inApp']!, + onChanged: (val) { + setState(() { + _notificationPrefs[prefKey]!['inApp'] = val ?? false; + }); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildCheckboxRow({ + required String label, + required bool value, + required ValueChanged onChanged, + }) { + return GestureDetector( + onTap: () => onChanged(!value), + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 18, + height: 18, + child: Checkbox( + value: value, + onChanged: onChanged, + activeColor: AppColors.primaryDark, + checkColor: Colors.white, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + ), + side: BorderSide( + color: AppColors.primaryDark.withValues(alpha: 0.4), + width: 1, + ), + ), + ), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ); + } + + /// Figma: h=32, w=278, rounded-[7px], with vertical dividers and inset orange pill + Widget _buildFrequencySelector() { + const options = ['instant', 'daily', 'weekly', 'off']; + const labels = ['Instant', 'Daily', 'Weekly', 'Off']; + + return Container( + height: 32, + width: 278, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.15), + width: 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Row( + children: List.generate(options.length * 2 - 1, (index) { + // Odd indices = vertical dividers + if (index.isOdd) { + return Container( + width: 1, + height: 32, + color: AppColors.primaryDark.withValues(alpha: 0.15), + ); + } + final i = index ~/ 2; + final isSelected = _digestFrequency == options[i]; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _digestFrequency = options[i]), + child: Center( + child: isSelected + ? Container( + height: 22, + margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + child: Text( + labels[i], + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ) + : Text( + labels[i], + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + ); + }), + ), + ), + ); + } + + /// Notification tab buttons — Figma: rounded-[15px] (different from profile tab) + Widget _buildNotificationActionButtons() { + final isSaving = ref.watch(profileProvider).isSaving; + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.15), + width: 1, + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => context.pop(), + child: Container( + height: 31, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.15), + width: 1, + ), + ), + alignment: Alignment.center, + child: const Text( + 'Cancel', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: GestureDetector( + onTap: isSaving ? null : _saveNotificationSettings, + child: Container( + height: 31, + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + child: isSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primaryDark, + ), + ) + : const Text( + 'Save Changes', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + ), + ], + ), + ); + } + + void _saveNotificationSettings() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Notification settings saved'), + backgroundColor: Color(0xFF638559), + ), + ); + } + + // ── Privacy Tab ── + + Widget _buildPrivacyTab() { + return const Center( + child: Padding( + padding: EdgeInsets.all(24), + child: Text( + 'Privacy settings coming soon', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 16, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ); + } + + // ── Shared Action Buttons ── + + Widget _buildActionButtons({required VoidCallback onSave}) { + final isSaving = ref.watch(profileProvider).isSaving; + + return Container( + height: 71, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Container( + width: 143, + height: 31, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + border: Border.all( + color: AppColors.primaryDark.withValues(alpha: 0.1), + width: 1, + ), + ), + alignment: Alignment.center, + child: const Text( + 'Cancel', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: isSaving ? null : onSave, + child: Container( + width: 143, + height: 31, + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(7), + ), + alignment: Alignment.center, + child: isSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primaryDark, + ), + ) + : const Text( + 'Save Changes', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + ), + ], + ), + ); + } + + // ── 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 23e8d40..1a287f4 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -10,6 +10,9 @@ 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/contact/presentation/screens/contact_screen.dart'; +import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart'; final routerProvider = Provider((ref) { final authRefreshNotifier = _AuthRefreshNotifier(); @@ -23,7 +26,7 @@ final routerProvider = Provider((ref) { }); // Public routes accessible without authentication (matching web middleware) - const publicRoutes = ['/home', '/agents/search', '/agents/detail', '/faq']; + const publicRoutes = ['/home', '/agents/search', '/agents/detail', '/faq', '/contact']; const authRoutes = ['/login', '/signup']; return GoRouter( @@ -81,21 +84,34 @@ final routerProvider = Provider((ref) { return AgentDetailScreen(agentId: id); }, ), - GoRoute( - path: '/messages', - builder: (context, state) => const ConversationsScreen(), - ), - GoRoute( - path: '/messages/chat/:id', - builder: (context, state) { - final id = state.pathParameters['id']!; - return ChatScreen(conversationId: id); - }, + ShellRoute( + builder: (context, state, child) => MessagingShell(child: child), + routes: [ + GoRoute( + path: '/messages', + builder: (context, state) => const ConversationsScreen(), + ), + GoRoute( + path: '/messages/chat/:id', + builder: (context, state) { + final id = state.pathParameters['id']!; + return ChatScreen(conversationId: id); + }, + ), + ], ), GoRoute( path: '/faq', builder: (context, state) => const FaqScreen(), ), + GoRoute( + path: '/contact', + builder: (context, state) => const ContactScreen(), + ), + GoRoute( + path: '/profile', + builder: (context, state) => const ProfileSettingsScreen(), + ), ], ); });