diff --git a/assets/icons/agents_tab_icon.svg b/assets/icons/agents_tab_icon.svg new file mode 100644 index 0000000..b2efd0b --- /dev/null +++ b/assets/icons/agents_tab_icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/hire_quickly_icon.svg b/assets/icons/hire_quickly_icon.svg new file mode 100644 index 0000000..036ae4a --- /dev/null +++ b/assets/icons/hire_quickly_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/lenders_tab_icon.svg b/assets/icons/lenders_tab_icon.svg new file mode 100644 index 0000000..9640947 --- /dev/null +++ b/assets/icons/lenders_tab_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/location_filled_icon.svg b/assets/icons/location_filled_icon.svg new file mode 100644 index 0000000..c35a3d2 --- /dev/null +++ b/assets/icons/location_filled_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/location_pin_icon.svg b/assets/icons/location_pin_icon.svg new file mode 100644 index 0000000..d920e1c --- /dev/null +++ b/assets/icons/location_pin_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/network_icon.svg b/assets/icons/network_icon.svg new file mode 100644 index 0000000..d4205c2 Binary files /dev/null and b/assets/icons/network_icon.svg differ diff --git a/assets/icons/notification_icon.svg b/assets/icons/notification_icon.svg new file mode 100644 index 0000000..9993c80 --- /dev/null +++ b/assets/icons/notification_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/search_icon.svg b/assets/icons/search_icon.svg new file mode 100644 index 0000000..4572b2c --- /dev/null +++ b/assets/icons/search_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/star_rating_icon.svg b/assets/icons/star_rating_icon.svg new file mode 100644 index 0000000..6ed4e0f --- /dev/null +++ b/assets/icons/star_rating_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/testimonial_bg.svg b/assets/icons/testimonial_bg.svg new file mode 100644 index 0000000..b09317c --- /dev/null +++ b/assets/icons/testimonial_bg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/top_rated_icon.svg b/assets/icons/top_rated_icon.svg new file mode 100644 index 0000000..3ccea76 --- /dev/null +++ b/assets/icons/top_rated_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/trusted_icon.svg b/assets/icons/trusted_icon.svg new file mode 100644 index 0000000..b1686b0 --- /dev/null +++ b/assets/icons/trusted_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/verified_badge_icon.svg b/assets/icons/verified_badge_icon.svg new file mode 100644 index 0000000..101d464 --- /dev/null +++ b/assets/icons/verified_badge_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/verified_icon.svg b/assets/icons/verified_icon.svg new file mode 100644 index 0000000..0efa27b --- /dev/null +++ b/assets/icons/verified_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/agent_card_image.png b/assets/images/agent_card_image.png new file mode 100644 index 0000000..b984fd3 Binary files /dev/null and b/assets/images/agent_card_image.png differ diff --git a/assets/images/agent_profile_image.png b/assets/images/agent_profile_image.png new file mode 100644 index 0000000..4c913aa Binary files /dev/null and b/assets/images/agent_profile_image.png differ diff --git a/assets/images/footer_bg.png b/assets/images/footer_bg.png new file mode 100644 index 0000000..3cd2ce6 --- /dev/null +++ b/assets/images/footer_bg.png @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/images/hero_bg.png b/assets/images/hero_bg.png new file mode 100644 index 0000000..bda285b Binary files /dev/null and b/assets/images/hero_bg.png differ diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart index 688cbd6..5197c58 100644 --- a/lib/features/auth/data/auth_repository.dart +++ b/lib/features/auth/data/auth_repository.dart @@ -52,6 +52,32 @@ class AuthRepository { } } + Future getMe() async { + try { + final response = await _dio.get(ApiConstants.authMe); + final data = response.data['data'] as Map; + return UserModel.fromJson(data); + } on DioException catch (e) { + if (e.error is ApiException) { + throw e.error as ApiException; + } + throw ApiException( + message: e.message ?? 'Failed to fetch user', + statusCode: e.response?.statusCode, + ); + } + } + + Future logout() async { + try { + await _dio.post(ApiConstants.authLogout); + } catch (_) { + // Ignore logout API errors — still clear local tokens + } finally { + await SecureStorage.clearTokens(); + } + } + Future register(RegisterRequest request) async { try { final response = await _dio.post( diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index daacfad..ae4e16d 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -1,11 +1,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:real_estate_mobile/core/network/api_exceptions.dart'; +import 'package:real_estate_mobile/core/storage/secure_storage.dart'; import 'package:real_estate_mobile/features/auth/data/auth_repository.dart'; import 'package:real_estate_mobile/features/auth/data/models/register_request.dart'; import 'package:real_estate_mobile/features/auth/data/models/user_model.dart'; // Auth state -enum AuthStatus { initial, loading, success, error } +enum AuthStatus { initial, loading, authenticated, error } class AuthState { final AuthStatus status; @@ -49,7 +50,37 @@ class AuthState { class AuthNotifier extends StateNotifier { final AuthRepository _repository; - AuthNotifier(this._repository) : super(const AuthState()); + AuthNotifier(this._repository) : super(const AuthState()) { + checkAuthStatus(); + } + + Future checkAuthStatus() async { + 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( + status: AuthStatus.authenticated, + user: user, + ); + } catch (_) { + // Token is invalid/expired — clear and show login + await SecureStorage.clearTokens(); + state = state.copyWith(status: AuthStatus.initial); + } + } + + Future logout() async { + state = state.copyWith(status: AuthStatus.loading); + await _repository.logout(); + state = const AuthState(); + } Future login({ required String email, @@ -76,7 +107,7 @@ class AuthNotifier extends StateNotifier { } state = state.copyWith( - status: AuthStatus.success, + status: AuthStatus.authenticated, user: result['user'] as UserModel, ); } on ApiException catch (e) { @@ -124,7 +155,7 @@ class AuthNotifier extends StateNotifier { ); state = state.copyWith( - status: AuthStatus.success, + status: AuthStatus.authenticated, user: user, registeredEmail: email, ); diff --git a/lib/features/auth/presentation/screens/login_screen.dart b/lib/features/auth/presentation/screens/login_screen.dart index af0c932..6fdbd2e 100644 --- a/lib/features/auth/presentation/screens/login_screen.dart +++ b/lib/features/auth/presentation/screens/login_screen.dart @@ -16,7 +16,6 @@ class LoginScreen extends ConsumerStatefulWidget { } class _LoginScreenState extends ConsumerState { - final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); String _selectedRole = 'USER'; @@ -24,7 +23,6 @@ class _LoginScreenState extends ConsumerState { @override void dispose() { - _nameController.dispose(); _emailController.dispose(); _passwordController.dispose(); super.dispose(); @@ -63,6 +61,8 @@ class _LoginScreenState extends ConsumerState { final authState = ref.watch(authProvider); final isLoading = authState.status == AuthStatus.loading; + // Router redirect handles navigation on auth state change + return Scaffold( body: Container( width: double.infinity, @@ -149,14 +149,6 @@ class _LoginScreenState extends ConsumerState { const SizedBox(height: 16), ], - // Name field - AuthTextField( - hintText: 'Name', - controller: _nameController, - keyboardType: TextInputType.name, - ), - const SizedBox(height: 16), - // Email field AuthTextField( hintText: 'Email', diff --git a/lib/features/auth/presentation/screens/signup_screen.dart b/lib/features/auth/presentation/screens/signup_screen.dart index 7ef2df6..148992e 100644 --- a/lib/features/auth/presentation/screens/signup_screen.dart +++ b/lib/features/auth/presentation/screens/signup_screen.dart @@ -81,7 +81,7 @@ class _SignupScreenState extends ConsumerState { child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24), - child: authState.status == AuthStatus.success + child: authState.status == AuthStatus.authenticated ? _buildSuccessContent(authState) : _buildFormContent(authState), ), diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart new file mode 100644 index 0000000..58975cd --- /dev/null +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.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 StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + // Header + SafeArea( + bottom: false, + child: const HomeHeader(), + ), + + // Hero Section with Search + const HeroSection(), + const SizedBox(height: 40), + + // Features Section + const FeaturesSection(), + const SizedBox(height: 40), + + // Top Professionals Section + const TopProfessionalsSection(), + const SizedBox(height: 40), + + // Trust & Stats Section + const TrustStatsSection(), + const SizedBox(height: 40), + + // Testimonials Section + const TestimonialsSection(), + const SizedBox(height: 40), + + // Footer + _buildFooter(), + ], + ), + ), + ); + } + + Widget _buildFooter() { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + color: AppColors.primaryDark, + child: Column( + children: [ + Image.asset( + 'assets/icons/logo.png', + width: 109, + height: 29, + color: Colors.white, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildFooterLink('Home'), + _buildFooterDot(), + _buildFooterLink('About'), + _buildFooterDot(), + _buildFooterLink('Contact'), + _buildFooterDot(), + _buildFooterLink('FAQ'), + ], + ), + const SizedBox(height: 16), + Text( + '\u00A9 2025 RE-QuestN. All rights reserved.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.white.withValues(alpha: 0.6), + ), + ), + ], + ), + ); + } + + Widget _buildFooterLink(String text) { + return Text( + text, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.white, + ), + ); + } + + Widget _buildFooterDot() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + '\u2022', + style: TextStyle( + fontSize: 14, + color: Colors.white.withValues(alpha: 0.5), + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/features_section.dart b/lib/features/home/presentation/widgets/features_section.dart new file mode 100644 index 0000000..c35f3ae --- /dev/null +++ b/lib/features/home/presentation/widgets/features_section.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; + +class FeaturesSection extends StatelessWidget { + const FeaturesSection({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const Text( + 'Find Trusted Real Estate Professionals On Demand', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 25, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + const Text( + 'Quickly connect with the right agents exactly when you need them.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 24), + _buildFeatureCard( + icon: 'assets/icons/hire_quickly_icon.svg', + fallbackIcon: Icons.bolt, + title: 'Hire Quickly', + description: + 'Connect with experienced local real estate agents who match your needs and preferences. Our simple process helps you get started quickly and move forward without delays or unnecessary complexity.', + ), + const SizedBox(height: 12), + _buildFeatureCard( + icon: 'assets/icons/verified_icon.svg', + fallbackIcon: Icons.verified_user_outlined, + title: 'Verified Agents', + description: + 'All agents are carefully identity-verified and background-checked to ensure trust and safety. You can confidently work with professionals who meet quality and reliability standards.', + ), + const SizedBox(height: 12), + _buildFeatureCard( + icon: 'assets/icons/top_rated_icon.svg', + fallbackIcon: Icons.star_outline, + title: 'Top Rated', + description: + 'Work with highly rated agents known for strong expertise and positive client feedback. Their consistent performance helps you make informed and confident property decisions.', + ), + const SizedBox(height: 12), + _buildFeatureCard( + icon: 'assets/icons/trusted_icon.svg', + fallbackIcon: Icons.people_outline, + title: 'Trusted by Thousands', + description: + 'Thousands of buyers, sellers, and renters trust our platform to connect with reliable, verified agents. Our experienced professionals support every step of the property journey, helping users make confident decisions with clarity, trust, and ease.', + ), + ], + ), + ); + } + + Widget _buildFeatureCard({ + required String icon, + required IconData fallbackIcon, + required String title, + required String description, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppColors.primaryDark, + width: 0.1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset( + icon, + width: 35, + height: 35, + placeholderBuilder: (_) => Icon( + fallbackIcon, + size: 35, + color: AppColors.accentOrange, + ), + ), + const SizedBox(width: 12), + Text( + title, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 20, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + description, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/hero_section.dart b/lib/features/home/presentation/widgets/hero_section.dart new file mode 100644 index 0000000..c8ce1db --- /dev/null +++ b/lib/features/home/presentation/widgets/hero_section.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; + +class HeroSection extends StatelessWidget { + const HeroSection({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Background image + ClipRRect( + borderRadius: BorderRadius.circular(0), + child: Image.asset( + 'assets/images/hero_bg.png', + width: double.infinity, + height: 580, + fit: BoxFit.cover, + ), + ), + // Content overlay + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + children: [ + const SizedBox(height: 40), + const Text( + 'Discover verified, top-rated real estate professionals.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 30, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + height: 1.1, + ), + ), + const SizedBox(height: 30), + // Search Agents Card + _buildSearchCard(), + ], + ), + ), + ], + ); + } + + Widget _buildSearchCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Text( + 'Search Agents', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + ), + const SizedBox(height: 16), + _buildSearchField('Types'), + const SizedBox(height: 12), + _buildSearchField('Name or Keyword'), + const SizedBox(height: 12), + _buildSearchField('Location'), + const SizedBox(height: 12), + _buildSearchField('Categories'), + const SizedBox(height: 12), + // See All Agents button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryDark, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + 'See All Agents', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildSearchField(String hint) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Expanded( + child: Text( + hint, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ), + if (hint == 'Types' || hint == 'Categories') + const Icon( + Icons.keyboard_arrow_down, + color: AppColors.primaryDark, + size: 20, + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/home_header.dart b/lib/features/home/presentation/widgets/home_header.dart new file mode 100644 index 0000000..cbf0164 --- /dev/null +++ b/lib/features/home/presentation/widgets/home_header.dart @@ -0,0 +1,262 @@ +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/features/auth/presentation/providers/auth_provider.dart'; + +class HomeHeader extends StatelessWidget { + const HomeHeader({super.key}); + + @override + Widget build(BuildContext context) { + return 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: [ + // Logo + Image.asset( + 'assets/icons/logo.png', + width: 109, + height: 29, + ), + // Right side: notification + hamburger menu + Row( + children: [ + // Notification bell + GestureDetector( + onTap: () {}, + child: const Icon( + Icons.notifications_outlined, + color: AppColors.primaryDark, + size: 26, + ), + ), + const SizedBox(width: 16), + // Hamburger menu + GestureDetector( + onTap: () => _openDrawer(context), + child: const Icon( + Icons.menu, + color: AppColors.primaryDark, + size: 26, + ), + ), + ], + ), + ], + ), + ); + } + + void _openDrawer(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const _MenuDrawer(), + ); + } +} + +class _MenuDrawer extends ConsumerWidget { + const _MenuDrawer(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Handle bar + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.divider, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + // Header with logo and close + 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), + ), + child: const Icon( + Icons.close, + color: AppColors.primaryDark, + size: 20, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + const Divider(color: AppColors.divider, height: 1), + // Menu items + Expanded( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + children: [ + _buildMenuItem( + context, + icon: Icons.home_outlined, + label: 'Home', + onTap: () { + Navigator.pop(context); + context.go('/home'); + }, + ), + _buildMenuItem( + context, + icon: Icons.school_outlined, + label: 'Education', + onTap: () => Navigator.pop(context), + ), + _buildMenuItem( + context, + icon: Icons.info_outline, + label: 'About Us', + onTap: () => Navigator.pop(context), + ), + _buildMenuItem( + context, + icon: Icons.help_outline, + label: "FAQ's", + onTap: () => Navigator.pop(context), + ), + _buildMenuItem( + context, + icon: Icons.mail_outline, + label: 'Contact', + onTap: () => Navigator.pop(context), + ), + const SizedBox(height: 16), + const Divider(color: AppColors.divider, height: 1), + const SizedBox(height: 16), + _buildMenuItem( + context, + icon: Icons.person_outline, + label: 'Account Settings', + onTap: () => Navigator.pop(context), + ), + _buildMenuItem( + context, + icon: Icons.notifications_outlined, + label: 'Notification Settings', + onTap: () => Navigator.pop(context), + ), + const SizedBox(height: 16), + const Divider(color: AppColors.divider, height: 1), + const SizedBox(height: 24), + // Log Out button + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + Navigator.pop(context); + ref.read(authProvider.notifier).logout(); + }, + icon: const Icon( + Icons.logout, + color: AppColors.accentOrange, + size: 20, + ), + label: const Text( + 'Log Out', + 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), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildMenuItem( + BuildContext context, { + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + children: [ + Icon(icon, color: AppColors.primaryDark, size: 24), + const SizedBox(width: 16), + Text( + label, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + const Spacer(), + const Icon( + Icons.chevron_right, + color: AppColors.hintText, + size: 22, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/testimonials_section.dart b/lib/features/home/presentation/widgets/testimonials_section.dart new file mode 100644 index 0000000..36058d8 --- /dev/null +++ b/lib/features/home/presentation/widgets/testimonials_section.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; + +class TestimonialsSection extends StatelessWidget { + const TestimonialsSection({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Testimonial card + SizedBox( + height: 380, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 24), + itemCount: 3, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(right: 16), + child: _buildTestimonialCard(), + ), + ), + ), + ], + ); + } + + Widget _buildTestimonialCard() { + return Container( + width: 315, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColors.primaryDark, + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quote icon + const Text( + '\u201C', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 48, + fontWeight: FontWeight.w700, + color: AppColors.accentOrange, + height: 0.8, + ), + ), + const SizedBox(height: 8), + const Text( + 'I have been working with Lorem ..', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(height: 12), + const Expanded( + child: Text( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 16), + // Stars + Row( + children: List.generate( + 5, + (index) => const Padding( + padding: EdgeInsets.only(right: 4), + child: Icon( + Icons.star, + color: AppColors.accentOrange, + size: 18, + ), + ), + ), + ), + const SizedBox(height: 12), + // Author info + const Text( + 'Conor Kenney', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + const Text( + 'Chief Operations Officer', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/top_professionals_section.dart b/lib/features/home/presentation/widgets/top_professionals_section.dart new file mode 100644 index 0000000..8af149f --- /dev/null +++ b/lib/features/home/presentation/widgets/top_professionals_section.dart @@ -0,0 +1,433 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; + +class TopProfessionalsSection extends StatelessWidget { + const TopProfessionalsSection({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const Text( + '"Meet top real estate professionals"', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 25, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 24), + + // Agents / Lenders Tab + _buildCategoryTab(), + const SizedBox(height: 24), + + // Agent Card + _buildAgentCard(), + const SizedBox(height: 12), + + // Progress indicator + _buildProgressIndicator(), + const SizedBox(height: 32), + + // See All Agents CTA + _buildSeeAllAgentsCta(), + ], + ), + ); + } + + Widget _buildCategoryTab() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.1), + ), + child: Row( + children: [ + // Active tab - Agents + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/icons/agents_tab_icon.svg', + width: 24, + height: 24, + placeholderBuilder: (_) => const Icon( + Icons.people, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 8), + const Text( + 'Agents', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 17, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ), + ), + // Inactive tab - Lenders + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/icons/lenders_tab_icon.svg', + width: 24, + height: 24, + placeholderBuilder: (_) => const Icon( + Icons.calendar_today, + color: AppColors.primaryDark, + size: 24, + ), + ), + const SizedBox(width: 8), + const Text( + 'Lenders', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 17, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildAgentCard() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Agent image + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), + child: Image.asset( + 'assets/images/agent_profile_image.png', + width: double.infinity, + height: 192, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: double.infinity, + height: 192, + color: AppColors.gradientStart, + child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name and badges row + Row( + children: [ + const Text( + 'Arjun Mehta', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 8), + SvgPicture.asset( + 'assets/icons/verified_badge_icon.svg', + width: 16, + height: 16, + placeholderBuilder: (_) => const Icon( + Icons.verified, + color: Colors.blue, + size: 16, + ), + ), + const Spacer(), + SvgPicture.asset( + 'assets/icons/star_rating_icon.svg', + width: 16, + height: 16, + placeholderBuilder: (_) => const Icon( + Icons.star, + color: AppColors.accentOrange, + size: 16, + ), + ), + const SizedBox(width: 4), + const Text( + '4.9 Rating', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + const SizedBox(height: 4), + // Verified and Location + const Text( + '"Verified local agent"', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF638559), + ), + ), + const SizedBox(height: 8), + // Location + Row( + children: [ + const Text( + 'Location:', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 4), + SvgPicture.asset( + 'assets/icons/location_pin_icon.svg', + width: 14, + height: 14, + placeholderBuilder: (_) => const Icon( + Icons.location_on, + color: AppColors.accentOrange, + size: 14, + ), + ), + const SizedBox(width: 4), + const Text( + 'San Francisco, CA', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 10, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + const SizedBox(height: 8), + // Expertise + Row( + children: [ + const Text( + 'Expertise:', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 8), + const Text( + '(Residential Property Expert)', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 10, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + const SizedBox(height: 12), + // Tags + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildTag('Residential'), + _buildTag('Rental'), + _buildTag('Commercial'), + _buildTag('Inspection'), + _buildTag('Land'), + _buildTag('Rental'), + ], + ), + const SizedBox(height: 12), + // Experience + RichText( + text: const TextSpan( + children: [ + TextSpan( + text: 'Experience: ', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + TextSpan( + text: '10+ years in the real estate industry.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTag(String label) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.7), + ), + child: Text( + label, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 13, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ); + } + + Widget _buildProgressIndicator() { + return Stack( + children: [ + Container( + height: 5, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.1), + ), + ), + Container( + height: 5, + width: 103, + decoration: BoxDecoration( + color: AppColors.primaryDark, + borderRadius: BorderRadius.circular(15), + ), + ), + ], + ); + } + + Widget _buildSeeAllAgentsCta() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.gradientStart, AppColors.gradientEnd], + ), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.2), + boxShadow: const [ + BoxShadow( + color: Color(0xFFD9D9D9), + offset: Offset(10, 0), + blurRadius: 20, + ), + ], + ), + child: Column( + children: [ + Image.asset( + 'assets/icons/network_icon.svg', + width: 30, + height: 23, + errorBuilder: (_, __, ___) => const Icon( + Icons.hub, + color: AppColors.primaryDark, + size: 30, + ), + ), + const SizedBox(height: 12), + const Text( + 'Discover 5,000+ Top Real Estate Agents in Our Network', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryDark, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + 'See All Agents', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/trust_stats_section.dart b/lib/features/home/presentation/widgets/trust_stats_section.dart new file mode 100644 index 0000000..843e4b3 --- /dev/null +++ b/lib/features/home/presentation/widgets/trust_stats_section.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; + +class TrustStatsSection extends StatelessWidget { + const TrustStatsSection({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const Text( + "Our Clients' Trust Is Our Foundation", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 25, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 16), + const Text( + 'We help buyers, sellers, and investors close successful real estate deals with confidence in every transaction.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + const Text( + 'Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 16), + const Divider(color: AppColors.divider), + const SizedBox(height: 16), + // Stats row + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Cities stat + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: const TextSpan( + children: [ + TextSpan( + text: '25+ Cities &\n', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + TextSpan( + text: 'neighborhoods served', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + RichText( + text: const TextSpan( + children: [ + TextSpan( + text: '1,000+ Properties\n', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + TextSpan( + text: 'bought and sold for our clients.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + ), + ], + ), + ), + // Rating visual + const SizedBox(width: 16), + Column( + children: [ + const Text( + '4.9', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 32, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + Row( + children: List.generate( + 5, + (index) => Icon( + index < 4 ? Icons.star : Icons.star_half, + color: AppColors.accentOrange, + size: 18, + ), + ), + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 0e94a7b..d12dfc8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,15 +10,17 @@ Future mainCommon(Flavor flavor) async { runApp(const ProviderScope(child: MyApp())); } -class MyApp extends StatelessWidget { +class MyApp extends ConsumerWidget { const MyApp({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); + return MaterialApp.router( title: 'RE-QuestN', theme: AppTheme.light, - routerConfig: AppRouter.router, + routerConfig: router, debugShowCheckedModeBanner: false, ); } diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index 18e092b..74d15d6 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -1,22 +1,64 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.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'; +import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart'; -class AppRouter { - AppRouter._(); +final routerProvider = Provider((ref) { + final authRefreshNotifier = _AuthRefreshNotifier(); - static final GoRouter router = GoRouter( - initialLocation: '/signup', + ref.listen(authProvider, (_, __) { + authRefreshNotifier.notify(); + }); + + ref.onDispose(() { + authRefreshNotifier.dispose(); + }); + + return GoRouter( + initialLocation: '/login', + refreshListenable: authRefreshNotifier, + redirect: (context, state) { + final authState = ref.read(authProvider); + final isAuthenticated = authState.status == AuthStatus.authenticated; + final isLoading = authState.status == AuthStatus.loading; + + final isAuthRoute = state.matchedLocation == '/login' || + state.matchedLocation == '/signup'; + + // While checking auth status, don't redirect + if (isLoading) return null; + + // If authenticated and on auth page, go to home + if (isAuthenticated && isAuthRoute) return '/home'; + + // If not authenticated and on protected page, go to login + if (!isAuthenticated && !isAuthRoute) return '/login'; + + return null; + }, routes: [ + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), GoRoute( path: '/signup', builder: (context, state) => const SignupScreen(), ), GoRoute( - path: '/login', - builder: (context, state) => const LoginScreen(), + path: '/home', + builder: (context, state) => const HomeScreen(), ), ], ); +}); + +/// Notifier that triggers GoRouter refresh when auth state changes. +class _AuthRefreshNotifier extends ChangeNotifier { + void notify() { + notifyListeners(); + } }