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:
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,14 +55,18 @@ 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');
|
||||
return;
|
||||
try {
|
||||
final repo = ref.read(agentsRepositoryProvider);
|
||||
agentTypes = await repo.getAgentTypes();
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
if (!mounted || agentTypes.isEmpty) return;
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user