Files
mobile-app/lib/features/agents/presentation/screens/agent_home_screen.dart

1237 lines
41 KiB
Dart
Raw Normal View History

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/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/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/data/profile_repository.dart';
/// Provider that fetches the agent's own profile ID from /agents/profile/me
final _agentMyProfileProvider =
FutureProvider.autoDispose<String?>((ref) async {
final repo = ProfileRepository();
try {
final data = await repo.getMyProfile('AGENT');
return data['id'] as String?;
} catch (_) {
return null;
}
});
class AgentHomeScreen extends ConsumerWidget {
const AgentHomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final myProfileAsync = ref.watch(_agentMyProfileProvider);
return myProfileAsync.when(
loading: () => const Scaffold(
backgroundColor: Colors.white,
body: Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
strokeWidth: 2,
),
),
),
error: (_, __) => Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline,
size: 48, color: AppColors.accentOrange),
const SizedBox(height: 16),
const Text('Failed to load profile',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark)),
const SizedBox(height: 12),
TextButton(
onPressed: () => ref.invalidate(_agentMyProfileProvider),
child: const Text('Retry',
style: TextStyle(color: AppColors.accentOrange)),
),
],
),
),
),
data: (agentProfileId) {
if (agentProfileId == null) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text('No agent profile found',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
color: AppColors.primaryDark)),
),
);
}
return _AgentHomeContent(agentProfileId: agentProfileId);
},
);
}
}
class _AgentHomeContent extends ConsumerStatefulWidget {
final String agentProfileId;
const _AgentHomeContent({required this.agentProfileId});
@override
ConsumerState<_AgentHomeContent> createState() => _AgentHomeContentState();
}
class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
@override
Widget build(BuildContext context) {
final state = ref.watch(agentDetailProvider(widget.agentProfileId));
return Scaffold(
backgroundColor: Colors.white,
body: state.isLoading
? const Center(
child: CircularProgressIndicator(color: AppColors.accentOrange))
: state.error != null
? _buildError(state.error!)
: _buildContent(state),
bottomNavigationBar: _buildBottomNavBar(),
);
}
Widget _buildError(String error) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline,
size: 48, color: AppColors.accentOrange),
const SizedBox(height: 16),
const Text('Unable to Load Profile',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(height: 12),
TextButton(
onPressed: () =>
ref.invalidate(agentDetailProvider(widget.agentProfileId)),
child: const Text('Retry',
style: TextStyle(color: AppColors.accentOrange)),
),
],
),
);
}
Widget _buildContent(AgentDetailState state) {
final agent = state.agent!;
return SingleChildScrollView(
child: Column(
children: [
SafeArea(bottom: false, child: const HomeHeader()),
const SizedBox(height: 12),
_buildCoverAndAvatar(agent),
const SizedBox(height: 16),
_buildStatusSection(agent),
const SizedBox(height: 12),
_buildContactInfo(state),
const SizedBox(height: 16),
_buildProfileInfo(agent),
const SizedBox(height: 24),
_buildExperienceSection(state),
const SizedBox(height: 24),
_buildInfoCards(state),
const SizedBox(height: 24),
_buildSpecializationSection(state),
const SizedBox(height: 24),
_buildTestimonialsSection(state),
const SizedBox(height: 24),
],
),
);
}
// ── Agent profile photo as cover image ──
Widget _buildCoverAndAvatar(AgentProfile agent) {
final screenWidth = MediaQuery.of(context).size.width;
final coverWidth = screenWidth * 0.877; // 377/430 from Figma
return Center(
child: Stack(
children: [
// Agent photo as the cover
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
width: coverWidth,
height: 346,
child: agent.avatarUrl != null && agent.avatarUrl!.isNotEmpty
? S3Image(
imageUrl: agent.avatarUrl!,
width: coverWidth,
height: 346,
fit: BoxFit.cover,
)
: Container(
color: const Color(0xFFC4D9D4),
alignment: Alignment.center,
child: Icon(Icons.person,
size: 80, color: AppColors.primaryDark),
),
),
),
// Orange edit icon top-right (tabler:edit style)
Positioned(
top: 12,
right: 10,
child: Container(
width: 28,
height: 26,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7),
),
alignment: Alignment.center,
child: SvgPicture.asset(
'assets/icons/edit_icon.svg',
width: 20,
height: 20,
),
),
),
],
),
);
}
// ── Available / Unavailable status ──
Widget _buildStatusSection(AgentProfile agent) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
_buildStatusRow(
isAvailable: true,
label: 'Available.',
buttonLabel: 'Connect',
buttonColor: AppColors.primaryDark,
buttonTextColor: Colors.white,
),
const SizedBox(height: 12),
_buildStatusRow(
isAvailable: false,
label: 'Unavailable.',
buttonLabel: 'Connect',
buttonColor: AppColors.accentOrange,
buttonTextColor: AppColors.primaryDark,
),
],
),
);
}
Widget _buildStatusRow({
required bool isAvailable,
required String label,
required String buttonLabel,
required Color buttonColor,
required Color buttonTextColor,
}) {
return Container(
height: 55,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: const Color(0xFFE0E0E0), width: 1),
),
child: Row(
children: [
const SizedBox(width: 20),
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isAvailable
? const Color(0xFF4CAF50)
: const Color(0xFFD0D0D0),
),
),
const SizedBox(width: 10),
Text(
label,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const Spacer(),
Container(
height: 55,
width: 164,
decoration: BoxDecoration(
color: buttonColor,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(
buttonLabel,
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: buttonTextColor,
),
),
),
],
),
);
}
// ── Contact Info ──
Widget _buildContactInfo(AgentDetailState state) {
final email = state.contactEmail ?? '';
final phone = state.contactPhone ?? '';
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.primaryDark, width: 0.1),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const Text('Email:',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(width: 16),
Expanded(
child: Text(
email.isNotEmpty ? state.maskEmail(email) : '-',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark),
),
),
const Icon(Icons.visibility_off_outlined,
size: 18, color: AppColors.primaryDark),
],
),
const SizedBox(height: 14),
Row(
children: [
const Text('Ph.No:',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(width: 16),
Expanded(
child: Text(
phone.isNotEmpty ? state.maskPhone(phone) : '-',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark),
),
),
const Icon(Icons.visibility_off_outlined,
size: 18, color: AppColors.primaryDark),
],
),
],
),
);
}
// ── Name, verified badge, title, location, member since, bio ──
Widget _buildProfileInfo(AgentProfile agent) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 38),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Name + Verified badge + (Verified Expert) on same line
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
agent.firstName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 4),
if (agent.isVerified) ...[
const Padding(
padding: EdgeInsets.only(bottom: 1),
child: Icon(Icons.verified,
size: 16, color: Color(0xFF2196F3)),
),
const SizedBox(width: 4),
],
Text(
agent.lastName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
if (agent.isVerified) ...[
const SizedBox(width: 6),
const Text(
'(Verified Expert)',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF638559),
),
),
],
],
),
// Title
if (agent.agentType != null) ...[
const SizedBox(height: 6),
Text(
agent.headline ?? agent.agentType!.name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
color: AppColors.primaryDark,
),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 8),
// Location + Member Since
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (agent.location.isNotEmpty) ...[
Icon(Icons.location_on,
size: 16,
color:
AppColors.primaryDark.withValues(alpha: 0.7)),
const SizedBox(width: 4),
Text(
agent.location,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 20),
],
Icon(Icons.calendar_today,
size: 14,
color:
AppColors.primaryDark.withValues(alpha: 0.7)),
const SizedBox(width: 4),
Text(
agent.memberSinceText,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
],
),
// Bio
if (agent.description.isNotEmpty) ...[
const SizedBox(height: 16),
SizedBox(
width: 338,
child: Text(
agent.description,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
height: 1.43,
),
textAlign: TextAlign.center,
),
),
],
],
),
// Edit icon positioned at top-right
Positioned(
right: 0,
top: 0,
child: GestureDetector(
onTap: () {},
child: SvgPicture.asset(
'assets/icons/edit_icon.svg',
width: 22,
height: 22,
),
),
),
],
),
);
}
// ── Experience Section ──
Widget _buildExperienceSection(AgentDetailState state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('Experience',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(height: 16),
_buildExperienceRow(
'Years in Experience', state.yearsInExperience),
const Divider(height: 24, thickness: 0.2),
_buildExperienceRow(
'Number of contract closed', state.contractsClosed),
const Divider(height: 24, thickness: 0.2),
if (state.licensingAreas.isNotEmpty) ...[
const Align(
alignment: Alignment.centerLeft,
child: Text('Licensing & Areas',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
),
const SizedBox(height: 8),
_buildTagsWrap(state.licensingAreas),
const SizedBox(height: 16),
],
if (state.expertiseYears.isNotEmpty) ...[
const Align(
alignment: Alignment.centerLeft,
child: Text('Areas in expertise & Years',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
),
const SizedBox(height: 8),
_buildTagsWrap(state.expertiseYears
.map((e) => e['years']!.isNotEmpty
? '${e['area']} ${e['years']}'
: e['area']!)
.toList()),
],
if (state.certifications.isNotEmpty) ...[
const SizedBox(height: 16),
const Align(
alignment: Alignment.centerLeft,
child: Text('Certifications',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
),
const SizedBox(height: 8),
...state.certifications.map((cert) => Padding(
padding: const EdgeInsets.only(bottom: 12, left: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('\u2022 ',
style: TextStyle(
fontSize: 14,
color: AppColors.primaryDark)),
Expanded(
child: Text(cert['name']!,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark)),
),
],
),
if (cert['org']!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 16),
child: Opacity(
opacity: 0.5,
child: Text(cert['org']!,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark)),
),
),
],
),
)),
],
],
),
);
}
Widget _buildExperienceRow(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
children: [
const Text('\u2022 ',
style: TextStyle(
fontSize: 15, color: AppColors.primaryDark)),
Text(value,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark)),
],
),
),
],
);
}
Widget _buildTagsWrap(List<String> tags) {
final displayTags = tags.take(6).toList();
final remaining = tags.length - 6;
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
...displayTags.map((tag) => Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border:
Border.all(color: AppColors.primaryDark, width: 0.7),
),
child: Text(tag,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark)),
)),
if (remaining > 0)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.primaryDark, width: 0.1),
),
child: Text('+$remaining More',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 15,
fontWeight: FontWeight.w300,
color: AppColors.primaryDark)),
),
],
);
}
// ── Info Cards (Availability, Work Environment, Experience) ──
Widget _buildInfoCards(AgentDetailState state) {
// Extract work environment and best experience from fieldValues
String workEnv = '';
String bestExp = '';
for (final fv in state.fieldValues) {
final slug = fv.fieldSlug.toLowerCase();
if (slug.contains('work_environment') ||
slug.contains('preferred_environment')) {
if (fv.textValue != null) workEnv = fv.textValue!;
if (fv.jsonValue is List) {
workEnv =
(fv.jsonValue as List).map((e) => e.toString()).join(', ');
}
}
if (slug.contains('best_experience') ||
slug.contains('amazing_experience')) {
if (fv.textValue != null) bestExp = fv.textValue!;
}
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 36),
child: Column(
children: [
// Availability card
if (state.availabilityType.isNotEmpty)
_buildInfoCard(
icon: Icons.access_time_filled,
title: 'Availability',
subtitle: state.availabilityType,
items: state.availabilitySchedule,
),
if (workEnv.isNotEmpty) ...[
const SizedBox(height: 16),
_buildInfoCard(
icon: Icons.landscape_outlined,
description: workEnv,
),
],
if (bestExp.isNotEmpty) ...[
const SizedBox(height: 16),
_buildInfoCard(
icon: Icons.star_rounded,
description: bestExp,
),
],
],
),
);
}
Widget _buildInfoCard({
required IconData icon,
String title = '',
String subtitle = '',
List<String>? items,
String? description,
}) {
return Container(
width: double.infinity,
constraints: const BoxConstraints(minHeight: 217),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.5),
offset: const Offset(0, 10),
blurRadius: 20,
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(icon, size: 36, color: AppColors.accentOrange),
if (title.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
],
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
],
if (items != null && items.isNotEmpty) ...[
const SizedBox(height: 8),
...items.map((item) => Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: Text(
item,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
),
),
)),
],
if (description != null && description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
description,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
textAlign: TextAlign.center,
),
],
],
),
);
}
// ── Specialization Section (grey background) ──
Widget _buildSpecializationSection(AgentDetailState state) {
final cards = state.specializationCards;
if (cards.isEmpty) return const SizedBox.shrink();
return Container(
width: double.infinity,
color: const Color(0xFFE6E6E6),
padding: const EdgeInsets.symmetric(vertical: 24),
child: Column(
children: [
const Text('Specialization',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(height: 4),
const Text('Area Of Expertise and Focus',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark)),
const SizedBox(height: 16),
...cards.map((card) => _buildSpecCard(card)),
],
),
);
}
Widget _buildSpecCard(SpecializationCard card) {
final displayName = AgentDetailState.titleCase(card.name);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 66, vertical: 8),
width: 298,
constraints: const BoxConstraints(minHeight: 236),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.primaryDark, width: 0.7),
),
child: Column(
children: [
const Icon(Icons.category_outlined,
size: 28, color: AppColors.accentOrange),
const SizedBox(height: 10),
Text(displayName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark)),
const SizedBox(height: 12),
...card.values.take(5).map((val) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
AgentDetailState.titleCase(val),
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
height: 1.8),
),
)),
if (card.values.length > 5) ...[
const SizedBox(height: 8),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border:
Border.all(color: AppColors.accentOrange, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Show More',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 10,
fontWeight: FontWeight.w700,
color: AppColors.accentOrange)),
const SizedBox(width: 4),
Icon(Icons.keyboard_arrow_down,
size: 14, color: AppColors.accentOrange),
],
),
),
],
],
),
);
}
// ── Testimonials Section ──
Widget _buildTestimonialsSection(AgentDetailState state) {
if (state.testimonials.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const Text(
'Testimonials',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1),
const SizedBox(height: 8),
const Text(
'Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
SizedBox(
height: 320,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.testimonials.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (_, i) =>
_TestimonialCard(data: state.testimonials[i]),
),
),
],
),
);
}
// ── 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: true,
onTap: () {},
),
_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.push('/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,
),
),
),
);
}
}
// ── Testimonial Card (same as agent detail screen) ──
class _TestimonialCard extends StatelessWidget {
final Map<String, dynamic> data;
const _TestimonialCard({required this.data});
@override
Widget build(BuildContext context) {
final text = data['text'] as String? ?? '';
final authorName = data['authorName'] as String? ?? '';
final authorRole = data['authorRole'] as String? ?? '';
final rating = (data['rating'] as num?)?.toInt() ?? 5;
return SizedBox(
width: 320,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Main card body with speech bubble shape
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.5),
offset: const Offset(0, 10),
blurRadius: 20,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Quote icon
const Text(
'\u201C',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 36,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
height: 0.8,
),
),
const SizedBox(height: 6),
// Title snippet
Text(
text.length > 35
? '${text.substring(0, 35)} ..'
: text,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
// Full text
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
height: 1.5,
),
overflow: TextOverflow.fade,
),
),
],
),
),
),
// Speech bubble triangle
Padding(
padding: const EdgeInsets.only(left: 30),
child: CustomPaint(
size: const Size(16, 10),
painter: _TrianglePainter(color: Colors.white),
),
),
const SizedBox(height: 6),
// Stars + Author info
Padding(
padding: const EdgeInsets.only(left: 4),
child: Row(
children: [
// Stars
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
5,
(i) => Icon(
i < rating ? Icons.star : Icons.star_border,
size: 16,
color: i < rating
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.3),
),
),
),
const SizedBox(width: 10),
// Author
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
authorName,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
if (authorRole.isNotEmpty)
Text(
authorRole,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
],
),
),
],
),
),
],
),
);
}
}
// ── Triangle painter for speech bubble ──
class _TrianglePainter extends CustomPainter {
final Color color;
_TrianglePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final shadowPaint = Paint()
..color = const Color(0xFFD9D9D9).withValues(alpha: 0.16)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4);
final path = Path()
..moveTo(0, 0)
..lineTo(size.width / 2, size.height)
..lineTo(size.width, 0)
..close();
canvas.drawPath(path, shadowPaint);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}