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: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/screens/home_screen.dart';
/// Determines which tab is active based on the current route.
int _currentTabIndex(BuildContext context) {
@@ -42,7 +43,24 @@ class AppBottomNavBar extends ConsumerWidget {
svgPath: 'assets/icons/nav_home_icon.svg',
fallbackIcon: Icons.home,
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(
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/presentation/providers/agent_detail_provider.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';
/// 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) {
return _buildError(state.error!);
}
return _buildContent(state);
return _buildContent(state, ref);
}
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 scrollController = ref.watch(homeScrollControllerProvider);
return SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
const SizedBox(height: 12),

View File

@@ -69,6 +69,7 @@ class ProfessionalItem {
final String experience;
final List<String> expertise;
final String imageUrl;
final double rating;
const ProfessionalItem({
required this.name,
@@ -77,6 +78,7 @@ class ProfessionalItem {
this.experience = '',
this.expertise = const [],
this.imageUrl = '',
this.rating = 5.0,
});
factory ProfessionalItem.fromJson(Map<String, dynamic> json) {
@@ -90,6 +92,9 @@ class ProfessionalItem {
.toList() ??
[],
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/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 {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final scrollController = ref.watch(homeScrollControllerProvider);
return SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
// Hero Section with Search

View File

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

View File

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