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

2074 lines
66 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/messaging/presentation/providers/messaging_provider.dart';
class AgentDetailScreen extends ConsumerStatefulWidget {
final String agentId;
const AgentDetailScreen({super.key, required this.agentId});
@override
ConsumerState<AgentDetailScreen> createState() => _AgentDetailScreenState();
}
class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
bool _emailVisible = false;
bool _phoneVisible = false;
bool _bioExpanded = false;
@override
void initState() {
super.initState();
_loadConnectionIfAuth();
}
void _loadConnectionIfAuth() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final authState = ref.read(authProvider);
if (authState.status == AuthStatus.authenticated) {
ref
.read(agentDetailProvider(widget.agentId).notifier)
.loadConnectionStatus();
}
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(agentDetailProvider(widget.agentId));
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.accentOrange),
);
}
if (state.error != null) {
return _buildError(state.error!);
}
return _buildContent(state);
}
Widget _buildError(String error) {
final isPermissionError = error == 'PERMISSION_DENIED';
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isPermissionError ? Icons.lock_outline : Icons.error_outline,
size: 48,
color: AppColors.accentOrange,
),
const SizedBox(height: 16),
Text(
isPermissionError ? 'Profile Not Available' : 'Unable to Load Profile',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
if (isPermissionError) ...[
const SizedBox(height: 8),
Text(
'This profile is private. You don\u2019t have permission to view it.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.7),
),
),
],
const SizedBox(height: 12),
TextButton(
onPressed: () => context.pop(),
child: const Text(
'Go Back',
style: TextStyle(color: AppColors.accentOrange),
),
),
],
),
),
);
}
Widget _buildContent(AgentDetailState state) {
final agent = state.agent!;
return RefreshIndicator(
color: AppColors.accentOrange,
onRefresh: () => ref
.read(agentDetailProvider(widget.agentId).notifier)
.refresh(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
// ── Back Button ──
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
context.go('/home');
}
},
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.primaryDark.withValues(alpha: 0.08),
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_back_ios_new_rounded,
color: AppColors.primaryDark,
size: 18,
),
),
),
),
),
// ── Avatar Section ──
const SizedBox(height: 8),
_buildAvatarSection(agent),
// ── Status + Connect Buttons ──
const SizedBox(height: 16),
_buildStatusButtons(state),
// ── Contact Info ──
const SizedBox(height: 12),
_buildContactInfo(state),
// ── Profile Info ──
const SizedBox(height: 16),
_buildProfileInfo(agent, state),
// ── Experience Section (always show, like web) ──
const SizedBox(height: 24),
_buildExperienceSection(state),
// ── Info Cards (Availability, Work Env, Best Experience) ──
const SizedBox(height: 24),
_buildInfoCards(state),
// ── Specialization Section (always show, like web) ──
const SizedBox(height: 24),
_buildSpecializationSection(state),
// ── Testimonials Section ──
if (state.testimonials.isNotEmpty) ...[
const SizedBox(height: 24),
_buildTestimonialsSection(state),
],
const SizedBox(height: 30),
],
),
),
);
}
// ── Avatar ──
Widget _buildAvatarSection(AgentProfile agent) {
final screenWidth = MediaQuery.of(context).size.width;
final avatarWidth = screenWidth * 0.877; // 377/430 from Figma
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
width: avatarWidth,
height: 346,
child: Stack(
fit: StackFit.expand,
children: [
S3Image(
imageUrl: agent.avatarUrl,
width: avatarWidth,
height: 346,
fit: BoxFit.cover,
errorWidget: (_) => Container(
color: const Color(0xFFC4D9D4),
child: const Icon(
Icons.person,
size: 80,
color: AppColors.primaryDark,
),
),
),
// Gradient overlay
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 100,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withValues(alpha: 0.5),
Colors.transparent,
],
),
),
),
),
],
),
),
),
);
}
// ── Status + Connect (single row matching web public view) ──
Widget _buildStatusButtons(AgentDetailState state) {
final agent = state.agent!;
final isAvailable = agent.isAvailable;
final connState = state.connectionState;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 38),
child: Column(
children: [
Container(
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
),
),
child: Row(
children: [
const SizedBox(width: 16),
// Status dot
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isAvailable
? const Color(0xFF22C55E)
: Colors.white,
border: Border.all(
color: isAvailable
? const Color(0xFF22C55E)
: AppColors.primaryDark.withValues(alpha: 0.3),
width: 1.5,
),
),
),
const SizedBox(width: 8),
Text(
isAvailable ? 'Available.' : 'Unavailable.',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const Spacer(),
// Connect/Pending/Unlink button
_buildConnectButton(
connState,
isAvailable,
() => _handleConnect(state),
),
const SizedBox(width: 4),
],
),
),
// Start Message button when connected
if (connState == 'accepted') ...[
const SizedBox(height: 12),
_buildStartMessageButton(agent),
],
],
),
);
}
bool _isStartingConversation = false;
void _handleStartMessage(AgentProfile agent) async {
if (_isStartingConversation) return;
setState(() => _isStartingConversation = true);
// Capture notifier before async gap to avoid using ref after dispose
final notifier = ref.read(messagingProvider.notifier);
try {
final conversationId = await notifier.startConversation(agent.id);
if (!mounted) return;
if (conversationId != null) {
context.push('/messages/chat/$conversationId');
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('You need to be connected to send messages.'),
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) setState(() => _isStartingConversation = false);
}
}
Widget _buildStartMessageButton(AgentProfile agent) {
return GestureDetector(
onTap: _isStartingConversation ? null : () => _handleStartMessage(agent),
child: Container(
height: 46,
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: _isStartingConversation
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.chat_bubble_outline, size: 18, color: Colors.white),
SizedBox(width: 8),
Text(
'Start Message',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
);
}
Widget _buildConnectButton(
String connState,
bool isAvailable,
VoidCallback? onConnect,
) {
Color bgColor;
Color textColor;
String label;
bool enabled;
switch (connState) {
case 'pending':
bgColor = const Color(0xFFFEF3C7); // yellow-100
textColor = const Color(0xFF92600F);
label = 'Pending';
enabled = false;
break;
case 'accepted':
bgColor = const Color(0xFFEF4444); // red-500
textColor = Colors.white;
label = 'Unlink';
enabled = true;
break;
default:
bgColor = isAvailable
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.7);
textColor = Colors.white;
label = 'Connect';
enabled = isAvailable;
}
return GestureDetector(
onTap: enabled ? onConnect : null,
child: Container(
height: 55,
width: 140,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(
label,
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: textColor,
),
),
),
);
}
void _handleConnect(AgentDetailState state) {
final authState = ref.read(authProvider);
if (authState.status != AuthStatus.authenticated) {
context.push('/login');
return;
}
if (state.connectionState == 'accepted') {
ref.read(agentDetailProvider(widget.agentId).notifier).cancelConnection();
return;
}
_showConnectModal(state);
}
void _showConnectModal(AgentDetailState state) {
final controller = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => _ConnectModalContent(
agentName: state.agent?.fullName ?? '',
controller: controller,
onSend: (message) async {
final success = await ref
.read(agentDetailProvider(widget.agentId).notifier)
.connect(message: message);
return success;
},
),
);
}
// ── Location Modal (matching web "Service Areas" modal) ──
void _showLocationModal(List<String> locations) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: EdgeInsets.fromLTRB(
24, 24, 24, 24 + MediaQuery.of(ctx).padding.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Service Areas',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
GestureDetector(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(
Icons.close,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: locations
.map((loc) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFE8E8E8),
borderRadius: BorderRadius.circular(10),
),
child: Text(
loc,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
color: AppColors.primaryDark,
),
),
))
.toList(),
),
],
),
),
);
}
// ── Contact Info ──
Widget _buildContactInfo(AgentDetailState state) {
final email = state.contactEmail;
final phone = state.contactPhone;
if (email == null && phone == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 38),
child: Container(
height: 98,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.15),
width: 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (email != null)
_buildContactRow(
label: 'Email:',
value: _emailVisible ? email : state.maskEmail(email),
onToggle: () => setState(() => _emailVisible = !_emailVisible),
visible: _emailVisible,
),
if (email != null && phone != null) const SizedBox(height: 12),
if (phone != null)
_buildContactRow(
label: 'Ph.No:',
value: _phoneVisible ? phone : state.maskPhone(phone),
onToggle: () => setState(() => _phoneVisible = !_phoneVisible),
visible: _phoneVisible,
),
],
),
),
);
}
Widget _buildContactRow({
required String label,
required String value,
required VoidCallback onToggle,
required bool visible,
}) {
return Row(
children: [
Text(
label,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: onToggle,
child: Icon(
visible ? Icons.visibility : Icons.visibility_off,
size: 18,
color: AppColors.primaryDark.withValues(alpha: 0.6),
),
),
],
);
}
// ── Profile Info (Name, Title, Location, Bio) ──
Widget _buildProfileInfo(AgentProfile agent, AgentDetailState state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 38),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Name + Verified badge + (Verified Expert) — badge after last name
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4,
runSpacing: 4,
children: [
Text(
'${agent.firstName} ${agent.lastName}',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
if (agent.isVerified) ...[
Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.verified,
size: 16,
color: Color(0xFF2196F3),
),
SizedBox(width: 4),
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 (wrapping) + Member Since (separate line)
Builder(builder: (_) {
var parts = state.locationParts;
// Fallback: use agent model location if fieldValues-based parts are empty
if (parts.isEmpty && agent.location.isNotEmpty) {
parts = agent.location.split(', ').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
}
const maxVisible = 4;
final visibleParts = parts.take(maxVisible).join(', ');
final remaining = parts.length - maxVisible;
return Column(
children: [
if (parts.isNotEmpty) ...[
GestureDetector(
onTap: remaining > 0 ? () => _showLocationModal(parts) : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: SvgPicture.asset(
'assets/icons/location_pin_icon.svg',
width: 18,
height: 18,
),
),
const SizedBox(width: 4),
Flexible(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: visibleParts,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
if (remaining > 0)
TextSpan(
text: ' +$remaining more',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.accentOrange,
),
),
],
),
textAlign: TextAlign.center,
),
),
],
),
),
const SizedBox(height: 6),
],
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
'assets/icons/calendar_icon.svg',
width: 18,
height: 18,
),
const SizedBox(width: 4),
Text(
agent.memberSinceText,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
],
),
],
);
}),
// Expertise tags (matching web ProfileCard)
if (state.expertiseTags.isNotEmpty) ...[
const SizedBox(height: 14),
_ExpandableTagsWrap(tags: state.expertiseTags),
],
// Bio with Show More / Show Less
if (state.descriptionText.isNotEmpty) ...[
const SizedBox(height: 16),
SizedBox(
width: 338,
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: _bioExpanded || state.descriptionText.length <= 150
? state.descriptionText
: '${state.descriptionText.substring(0, 150)}... ',
),
if (state.descriptionText.length > 150)
WidgetSpan(
child: GestureDetector(
onTap: () =>
setState(() => _bioExpanded = !_bioExpanded),
child: Text(
_bioExpanded ? ' Show Less' : 'Show More',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
decoration: TextDecoration.underline,
),
),
),
),
],
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
height: 1.43,
),
),
textAlign: TextAlign.center,
),
),
],
],
),
);
}
// ── Experience ──
Widget _buildExperienceSection(AgentDetailState state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 17),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Center(
child: Text(
'Experience',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 22),
// Years (always show, matching web)
_buildExperienceItem(state.yearsInExperienceLabel, [
state.yearsInExperience,
]),
// Contracts (always show, matching web)
_buildExperienceItem(state.contractsClosedLabel, [
state.contractsClosed,
]),
// Expertise Years (always show)
state.expertiseYears.isNotEmpty
? _buildExpertiseYears(state.expertiseYears, state.expertiseYearsLabel)
: _buildEmptySection(state.expertiseYearsLabel, 'No expertise areas added'),
// Certifications (always show)
state.certifications.isNotEmpty
? _buildCertifications(state.certifications, state.certificationsLabel)
: _buildEmptySection(state.certificationsLabel, 'No certifications added'),
],
),
);
}
Widget _buildExperienceItem(String title, List<String> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 14),
...items.map(
(item) => Padding(
padding: const EdgeInsets.only(left: 10, bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 2),
child: Text(
'\u2022 ',
style: TextStyle(
fontSize: 15,
color: AppColors.primaryDark,
),
),
),
Expanded(
child: Text(
item == '-' ? 'Not specified' : item,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 15,
fontWeight: FontWeight.w500,
color: item == '-'
? AppColors.primaryDark.withValues(alpha: 0.4)
: AppColors.primaryDark,
),
),
),
],
),
),
),
const SizedBox(height: 16),
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1,
),
const SizedBox(height: 10),
],
);
}
Widget _buildEmptySection(String title, String emptyText) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 14),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
emptyText,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark.withValues(alpha: 0.4),
),
),
),
const SizedBox(height: 16),
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1,
),
const SizedBox(height: 10),
],
);
}
Widget _buildLicensingAreas(List<String> areas, String label) {
return _ExpandableChipsSection(
title: label,
items: areas,
initialCount: 6,
);
}
Widget _buildExpertiseYears(List<Map<String, String>> items, String label) {
final chips = items.map((e) {
final years = e['years'] ?? '';
return years.isNotEmpty ? '${e['area']} $years' : e['area'] ?? '';
}).toList();
return _ExpandableChipsSection(
title: label,
items: chips,
initialCount: 4,
);
}
Widget _buildCertifications(List<Map<String, String>> certs, String label) {
return _ExpandableCertificationsSection(certs: certs, title: label);
}
// ── Info Cards (Availability, Work Env, Best Experience) ──
Widget _buildInfoCards(AgentDetailState state) {
// 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 (always show) — only schedule lines, no "Full-time" label
_buildInfoCard(
icon: Icons.access_time_filled,
title: state.availabilityLabel,
subtitle: state.availabilitySchedule.isEmpty ? 'Not specified' : '',
items: state.availabilitySchedule.isNotEmpty
? state.availabilitySchedule
: null,
isEmpty: state.availabilitySchedule.isEmpty,
),
const SizedBox(height: 16),
// Work environment (always show)
_buildInfoCard(
icon: Icons.landscape_outlined,
title: state.workEnvironmentLabel,
subtitle: '',
description: workEnv.isNotEmpty ? workEnv : 'Not specified',
isEmpty: workEnv.isEmpty,
),
const SizedBox(height: 16),
// Best experience (always show)
_buildInfoCard(
icon: Icons.star_rounded,
title: state.bestExperienceLabel,
subtitle: '',
description: bestExp.isNotEmpty ? bestExp : 'Not specified',
isEmpty: bestExp.isEmpty,
),
],
),
);
}
Widget _buildInfoCard({
required IconData icon,
required String title,
required String subtitle,
List<String>? items,
String? description,
bool isEmpty = false,
}) {
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: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w600,
color: isEmpty
? AppColors.primaryDark.withValues(alpha: 0.4)
: 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) ...[
const SizedBox(height: 8),
Text(
description,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w600,
color: isEmpty
? AppColors.primaryDark.withValues(alpha: 0.4)
: AppColors.primaryDark,
),
textAlign: TextAlign.center,
),
],
],
),
);
}
// ── Specialization Section ──
Widget _buildSpecializationSection(AgentDetailState state) {
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),
if (state.specializationCards.isNotEmpty)
...state.specializationCards.map(
(card) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _SpecializationCardWidget(card: card),
),
)
else
Text(
'No specialization data added',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark.withValues(alpha: 0.4),
),
),
],
),
);
}
// ── Testimonials ──
Widget _buildTestimonialsSection(AgentDetailState state) {
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: 380,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.testimonials.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (_, i) =>
_TestimonialCard(data: state.testimonials[i]),
),
),
],
),
);
}
}
// ── Expandable Chips Section ──
class _ExpandableTagsWrap extends StatefulWidget {
final List<String> tags;
final int initialCount;
const _ExpandableTagsWrap({required this.tags, this.initialCount = 6});
@override
State<_ExpandableTagsWrap> createState() => _ExpandableTagsWrapState();
}
class _ExpandableTagsWrapState extends State<_ExpandableTagsWrap> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
final displayTags =
_expanded ? widget.tags : widget.tags.take(widget.initialCount).toList();
final hasMore = widget.tags.length > widget.initialCount;
return Column(
children: [
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: displayTags
.map((tag) => Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
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.w400,
color: AppColors.primaryDark,
),
),
))
.toList(),
),
if (hasMore) ...[
const SizedBox(height: 10),
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.accentOrange, width: 0.7),
),
child: Text(
_expanded ? 'Show Less' : 'Show More',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 10,
fontWeight: FontWeight.w700,
color: AppColors.accentOrange,
),
),
),
),
],
],
);
}
}
class _ExpandableChipsSection extends StatefulWidget {
final String title;
final List<String> items;
final int initialCount;
const _ExpandableChipsSection({
required this.title,
required this.items,
this.initialCount = 6,
});
@override
State<_ExpandableChipsSection> createState() =>
_ExpandableChipsSectionState();
}
class _ExpandableChipsSectionState extends State<_ExpandableChipsSection> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
final showItems = _expanded
? widget.items
: widget.items.take(widget.initialCount).toList();
final hasMore = widget.items.length > widget.initialCount;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 14),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
...showItems.map(
(item) => IntrinsicWidth(
child: Container(
height: 28,
padding: const EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark,
width: 0.7,
),
),
child: Text(
item,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
),
),
),
if (hasMore || _expanded)
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: IntrinsicWidth(
child: Container(
height: 28,
padding: const EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: _expanded
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.15),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_expanded
? 'Show Less'
: '+${widget.items.length - widget.initialCount} More',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: _expanded ? 13 : 15,
fontWeight: _expanded
? FontWeight.w700
: FontWeight.w300,
color: _expanded
? AppColors.accentOrange
: AppColors.primaryDark,
),
),
if (_expanded) ...[
const SizedBox(width: 2),
const Icon(
Icons.keyboard_arrow_up,
size: 14,
color: AppColors.accentOrange,
),
],
],
),
),
),
),
],
),
const SizedBox(height: 14),
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1,
),
const SizedBox(height: 10),
],
);
}
}
// ── Expandable Certifications Section ──
class _ExpandableCertificationsSection extends StatefulWidget {
final List<Map<String, String>> certs;
final String title;
final int initialCount;
const _ExpandableCertificationsSection({
required this.certs,
required this.title,
this.initialCount = 2,
});
@override
State<_ExpandableCertificationsSection> createState() =>
_ExpandableCertificationsSectionState();
}
class _ExpandableCertificationsSectionState
extends State<_ExpandableCertificationsSection> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
final showCerts = _expanded
? widget.certs
: widget.certs.take(widget.initialCount).toList();
final hasMore = widget.certs.length > widget.initialCount;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
...showCerts.map(
(cert) => Padding(
padding: const EdgeInsets.only(left: 14, bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 2),
child: 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: 20, top: 2),
child: Text(
cert['org']!.toUpperCase(),
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
),
],
),
),
),
if (hasMore || _expanded)
Center(
child: GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.accentOrange, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_expanded
? 'Show Less'
: '+${widget.certs.length - widget.initialCount} More',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 10,
fontWeight: FontWeight.w700,
color: AppColors.accentOrange,
),
),
const SizedBox(width: 2),
Icon(
_expanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 14,
color: AppColors.accentOrange,
),
],
),
),
),
),
const SizedBox(height: 4),
],
);
}
}
// ── Specialization Card ──
class _SpecializationCardWidget extends StatefulWidget {
final SpecializationCard card;
const _SpecializationCardWidget({required this.card});
@override
State<_SpecializationCardWidget> createState() =>
_SpecializationCardWidgetState();
}
class _SpecializationCardWidgetState extends State<_SpecializationCardWidget> {
bool _expanded = false;
static const _initialCount = 3;
IconData get _icon {
final slug = widget.card.slug.toLowerCase();
if (slug.contains('loan') || slug.contains('price')) {
return Icons.attach_money;
}
if (slug.contains('property')) return Icons.apartment;
if (slug.contains('hobby') || slug.contains('interest')) {
return Icons.favorite_border;
}
return Icons.home_outlined;
}
@override
Widget build(BuildContext context) {
final values = _expanded
? widget.card.values
: widget.card.values.take(_initialCount).toList();
final hasMore = widget.card.values.length > _initialCount;
return Container(
width: 298,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.7),
width: 0.7,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_icon, size: 32, color: AppColors.accentOrange),
const SizedBox(height: 8),
Text(
widget.card.name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 12),
...values.map(
(v) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
AgentDetailState.titleCase(v),
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
height: 1.6,
),
textAlign: TextAlign.center,
),
),
),
if (hasMore) ...[
const SizedBox(height: 10),
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.accentOrange, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_expanded ? 'Show Less' : 'Show More',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 10,
fontWeight: FontWeight.w700,
color: AppColors.accentOrange,
),
),
const SizedBox(width: 2),
Icon(
_expanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 14,
color: AppColors.accentOrange,
),
],
),
),
),
],
],
),
);
}
}
// ── Testimonial Card (matches Figma & web TestimonialCard) ──
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 Container(
width: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Quote icon
SvgPicture.asset(
'assets/icons/quote_icon.svg',
width: 23,
height: 13,
),
const SizedBox(height: 12),
// Title snippet (bold)
Text(
text.length > 35 ? '${text.substring(0, 35)} ..' : text,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// Full text
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
height: 1.4,
),
overflow: TextOverflow.fade,
),
),
const SizedBox(height: 12),
// Divider
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.08),
height: 1,
),
const SizedBox(height: 12),
// Star rating
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
5,
(i) => Padding(
padding: const EdgeInsets.only(right: 2),
child: Icon(
i < rating ? Icons.star : Icons.star_border,
size: 18,
color: i < rating
? const Color(0xFFFFDE21)
: AppColors.primaryDark.withValues(alpha: 0.3),
),
),
),
),
const SizedBox(height: 8),
// Author name
Text(
authorName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
// Author role
if (authorRole.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
authorRole,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
],
],
),
);
}
}
// ── Connect Modal Content (matches web ConnectRequestModal) ──
class _ConnectModalContent extends StatefulWidget {
final String agentName;
final TextEditingController controller;
final Future<bool> Function(String? message) onSend;
const _ConnectModalContent({
required this.agentName,
required this.controller,
required this.onSend,
});
@override
State<_ConnectModalContent> createState() => _ConnectModalContentState();
}
class _ConnectModalContentState extends State<_ConnectModalContent> {
bool _isSubmitting = false;
bool _success = false;
String? _error;
void _closeModal() {
if (mounted) {
Navigator.of(context, rootNavigator: false).pop();
}
}
Future<void> _handleSend() async {
setState(() {
_isSubmitting = true;
_error = null;
});
try {
final success = await widget.onSend(widget.controller.text.trim());
if (success) {
setState(() => _success = true);
// Auto-close after 2 seconds (matching web)
Future.delayed(const Duration(seconds: 2), () {
_closeModal();
});
} else {
setState(() => _error = 'Failed to send connection request');
}
} catch (e) {
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 24),
child: _success ? _buildSuccessContent() : _buildFormContent(),
),
),
),
);
}
Widget _buildSuccessContent() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: const Color(0xFFDCFCE7), // green-100
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
size: 32,
color: Color(0xFF22C55E), // green-500
),
),
const SizedBox(height: 16),
const Text(
'Request Sent!',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
Text(
'Your connection request has been sent to ${widget.agentName}.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
],
);
}
Widget _buildFormContent() {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Connect with ${widget.agentName}',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
GestureDetector(
onTap: _closeModal,
child: Icon(
Icons.close,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
),
const SizedBox(height: 4),
Text(
'Send a connection request to start communicating with this agent.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.7),
),
),
const SizedBox(height: 16),
const Text(
'Message (Optional)',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
TextField(
controller: widget.controller,
maxLines: 4,
maxLength: 1000,
decoration: InputDecoration(
hintText:
'Introduce yourself and explain what you\'re looking for...',
hintStyle: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.4),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColors.primaryDark.withValues(alpha: 0.2),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColors.primaryDark.withValues(alpha: 0.2),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.accentOrange),
),
),
),
if (_error != null) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2), // red-50
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFECACA)), // red-200
),
child: Text(
_error!,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: Color(0xFFDC2626), // red-600
),
),
),
],
const SizedBox(height: 16),
Row(
children: [
// Cancel button
Expanded(
child: SizedBox(
height: 48,
child: OutlinedButton(
onPressed: _closeModal,
style: OutlinedButton.styleFrom(
side: BorderSide(
color: AppColors.primaryDark.withValues(alpha: 0.2),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: const Text(
'Cancel',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
),
),
),
const SizedBox(width: 12),
// Send Request button
Expanded(
child: SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _handleSend,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange,
foregroundColor: Colors.white,
disabledBackgroundColor:
AppColors.accentOrange.withValues(alpha: 0.5),
disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
_isSubmitting ? 'Sending...' : 'Send Request',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
],
),
],
);
}
}