feat: Add professional ratings to cards, implement scroll-to-top on home tab re-tap, and improve agent type fetching in the hero section.

This commit is contained in:
pradeepkumar
2026-03-15 22:28:59 +05:30
parent 1fafbb1eb4
commit f6263b833c
6 changed files with 76 additions and 17 deletions

View File

@@ -4,6 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:real_estate_mobile/core/constants/app_colors.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/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart';
/// Determines which tab is active based on the current route. /// Determines which tab is active based on the current route.
int _currentTabIndex(BuildContext context) { int _currentTabIndex(BuildContext context) {
@@ -42,7 +43,24 @@ class AppBottomNavBar extends ConsumerWidget {
svgPath: 'assets/icons/nav_home_icon.svg', svgPath: 'assets/icons/nav_home_icon.svg',
fallbackIcon: Icons.home, fallbackIcon: Icons.home,
isSelected: selectedIndex == 0, isSelected: selectedIndex == 0,
onTap: () => context.go('/home'), onTap: () {
if (selectedIndex == 0) {
// Already on home — scroll to top
try {
final controller =
ref.read(homeScrollControllerProvider);
if (controller.hasClients) {
controller.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
} catch (_) {}
} else {
context.go('/home');
}
},
), ),
_NavItem( _NavItem(
svgPath: 'assets/icons/nav_list_icon.svg', svgPath: 'assets/icons/nav_list_icon.svg',

View File

@@ -9,6 +9,7 @@ import 'package:real_estate_mobile/core/widgets/s3_image.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart';
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// Provider that fetches the agent's own profile ID from /agents/profile/me /// Provider that fetches the agent's own profile ID from /agents/profile/me
@@ -219,7 +220,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
if (state.error != null) { if (state.error != null) {
return _buildError(state.error!); return _buildError(state.error!);
} }
return _buildContent(state); return _buildContent(state, ref);
} }
Widget _buildError(String error) { Widget _buildError(String error) {
@@ -248,9 +249,11 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
); );
} }
Widget _buildContent(AgentDetailState state) { Widget _buildContent(AgentDetailState state, WidgetRef ref) {
final agent = state.agent!; final agent = state.agent!;
final scrollController = ref.watch(homeScrollControllerProvider);
return SingleChildScrollView( return SingleChildScrollView(
controller: scrollController,
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 12), const SizedBox(height: 12),

View File

@@ -69,6 +69,7 @@ class ProfessionalItem {
final String experience; final String experience;
final List<String> expertise; final List<String> expertise;
final String imageUrl; final String imageUrl;
final double rating;
const ProfessionalItem({ const ProfessionalItem({
required this.name, required this.name,
@@ -77,6 +78,7 @@ class ProfessionalItem {
this.experience = '', this.experience = '',
this.expertise = const [], this.expertise = const [],
this.imageUrl = '', this.imageUrl = '',
this.rating = 5.0,
}); });
factory ProfessionalItem.fromJson(Map<String, dynamic> json) { factory ProfessionalItem.fromJson(Map<String, dynamic> json) {
@@ -90,6 +92,9 @@ class ProfessionalItem {
.toList() ?? .toList() ??
[], [],
imageUrl: json['imageUrl'] as String? ?? '', imageUrl: json['imageUrl'] as String? ?? '',
rating: (json['rating'] as num?)?.toDouble() ??
(json['averageRating'] as num?)?.toDouble() ??
5.0,
); );
} }
} }

View File

@@ -8,12 +8,21 @@ import 'package:real_estate_mobile/features/home/presentation/widgets/testimonia
import 'package:real_estate_mobile/features/home/presentation/widgets/top_professionals_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'; import 'package:real_estate_mobile/features/home/presentation/widgets/trust_stats_section.dart';
/// Global scroll controller for the home screen so the bottom nav can scroll to top.
final homeScrollControllerProvider = Provider<ScrollController>((ref) {
final controller = ScrollController();
ref.onDispose(() => controller.dispose());
return controller;
});
class HomeScreen extends ConsumerWidget { class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final scrollController = ref.watch(homeScrollControllerProvider);
return SingleChildScrollView( return SingleChildScrollView(
controller: scrollController,
child: Column( child: Column(
children: [ children: [
// Hero Section with Search // Hero Section with Search

View File

@@ -55,15 +55,19 @@ class _HeroSectionState extends ConsumerState<HeroSection> {
context.push('/agents/search${queryString.isNotEmpty ? '?$queryString' : ''}'); context.push('/agents/search${queryString.isNotEmpty ? '?$queryString' : ''}');
} }
void _showTypePicker() { Future<void> _showTypePicker() async {
final topProState = ref.read(topProfessionalsProvider); var agentTypes = ref.read(topProfessionalsProvider).agentTypes;
final agentTypes = topProState.agentTypes;
// If types haven't loaded yet, fetch them directly
if (agentTypes.isEmpty) { if (agentTypes.isEmpty) {
// If types not loaded yet, just navigate to search try {
context.push('/agents/search'); final repo = ref.read(agentsRepositoryProvider);
agentTypes = await repo.getAgentTypes();
} catch (_) {
return; return;
} }
if (!mounted || agentTypes.isEmpty) return;
}
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,

View File

@@ -103,7 +103,7 @@ class _TopProfessionalsSectionState
else ...[ else ...[
// Professional Cards - horizontal scroll // Professional Cards - horizontal scroll
SizedBox( SizedBox(
height: 480, height: 540,
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -293,8 +293,10 @@ class _TopProfessionalsSectionState
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Image // Image (226px per Figma)
Expanded( SizedBox(
width: double.infinity,
height: 226,
child: ClipRRect( child: ClipRRect(
borderRadius: borderRadius:
const BorderRadius.vertical(top: Radius.circular(15)), const BorderRadius.vertical(top: Radius.circular(15)),
@@ -302,7 +304,7 @@ class _TopProfessionalsSectionState
? S3Image( ? S3Image(
imageUrl: imageUrl, imageUrl: imageUrl,
width: double.infinity, width: double.infinity,
height: double.infinity, height: 226,
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
placeholder: (_) => _buildImagePlaceholder(index), placeholder: (_) => _buildImagePlaceholder(index),
@@ -313,7 +315,8 @@ class _TopProfessionalsSectionState
), ),
// Content // Content
Padding( Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -468,9 +471,26 @@ class _TopProfessionalsSectionState
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
// Star Rating
Row(
children: List.generate(5, (i) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: Icon(
i < item.rating.round()
? Icons.star_rounded
: Icons.star_outline_rounded,
color: const Color(0xFFE5A625),
size: 24,
),
);
}),
),
], ],
), ),
), ),
),
], ],
), ),
); );
@@ -478,7 +498,7 @@ class _TopProfessionalsSectionState
Widget _buildShimmerCards() { Widget _buildShimmerCards() {
return SizedBox( return SizedBox(
height: 480, height: 540,
child: Shimmer.fromColors( child: Shimmer.fromColors(
baseColor: const Color(0xFFE0E0E0), baseColor: const Color(0xFFE0E0E0),
highlightColor: const Color(0xFFF5F5F5), highlightColor: const Color(0xFFF5F5F5),
@@ -492,7 +512,7 @@ class _TopProfessionalsSectionState
children: [ children: [
// Image placeholder // Image placeholder
Container( Container(
height: 192, height: 226,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(15)), borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
@@ -558,7 +578,7 @@ class _TopProfessionalsSectionState
Widget _buildAvatarPlaceholder() { Widget _buildAvatarPlaceholder() {
return Container( return Container(
width: double.infinity, width: double.infinity,
height: 192, height: 226,
color: AppColors.gradientStart, color: AppColors.gradientStart,
child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark), child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark),
); );