2026-03-08 00:41:44 +05:30
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
2026-03-14 20:59:47 +05:30
|
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
2026-03-08 00:41:44 +05:30
|
|
|
|
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';
|
2026-03-15 00:28:04 +05:30
|
|
|
|
import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart';
|
2026-03-08 00:41:44 +05:30
|
|
|
|
|
|
|
|
|
|
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;
|
2026-03-14 20:59:47 +05:30
|
|
|
|
bool _bioExpanded = false;
|
2026-03-08 00:41:44 +05:30
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
_loadConnectionIfAuth();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _loadConnectionIfAuth() {
|
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
|
final authState = ref.read(authProvider);
|
|
|
|
|
|
if (authState.status == AuthStatus.authenticated) {
|
2026-03-14 17:51:03 +05:30
|
|
|
|
ref
|
|
|
|
|
|
.read(agentDetailProvider(widget.agentId).notifier)
|
|
|
|
|
|
.loadConnectionStatus();
|
2026-03-08 00:41:44 +05:30
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final state = ref.watch(agentDetailProvider(widget.agentId));
|
|
|
|
|
|
|
2026-03-08 16:05:08 +05:30
|
|
|
|
if (state.isLoading) {
|
|
|
|
|
|
return const Center(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
child: CircularProgressIndicator(color: AppColors.accentOrange),
|
|
|
|
|
|
);
|
2026-03-08 16:05:08 +05:30
|
|
|
|
}
|
|
|
|
|
|
if (state.error != null) {
|
|
|
|
|
|
return _buildError(state.error!);
|
|
|
|
|
|
}
|
|
|
|
|
|
return _buildContent(state);
|
2026-03-08 00:41:44 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildError(String error) {
|
|
|
|
|
|
return Center(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(24),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
2026-03-14 17:51:03 +05:30
|
|
|
|
const Icon(
|
|
|
|
|
|
Icons.error_outline,
|
|
|
|
|
|
size: 48,
|
|
|
|
|
|
color: AppColors.accentOrange,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
const SizedBox(height: 16),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
const Text(
|
|
|
|
|
|
'Unable to Load Profile',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontFamily: 'Fractul',
|
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () => context.pop(),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
child: const Text(
|
|
|
|
|
|
'Go Back',
|
|
|
|
|
|
style: TextStyle(color: AppColors.accentOrange),
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildContent(AgentDetailState state) {
|
|
|
|
|
|
final agent = state.agent!;
|
2026-03-14 23:52:45 +05:30
|
|
|
|
return RefreshIndicator(
|
|
|
|
|
|
color: AppColors.accentOrange,
|
|
|
|
|
|
onRefresh: () => ref
|
|
|
|
|
|
.read(agentDetailProvider(widget.agentId).notifier)
|
|
|
|
|
|
.refresh(),
|
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
|
|
|
|
child: Column(
|
2026-03-08 00:41:44 +05:30
|
|
|
|
children: [
|
|
|
|
|
|
// ── Avatar Section ──
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
_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),
|
|
|
|
|
|
|
2026-03-14 23:41:50 +05:30
|
|
|
|
// ── Experience Section (always show, like web) ──
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
_buildExperienceSection(state),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
|
|
|
|
|
|
// ── Info Cards (Availability, Work Env, Best Experience) ──
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
_buildInfoCards(state),
|
|
|
|
|
|
|
2026-03-14 23:41:50 +05:30
|
|
|
|
// ── Specialization Section (always show, like web) ──
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
_buildSpecializationSection(state),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
|
|
|
|
|
|
// ── Testimonials Section ──
|
|
|
|
|
|
if (state.testimonials.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
_buildTestimonialsSection(state),
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-14 23:52:45 +05:30
|
|
|
|
),
|
2026-03-08 00:41:44 +05: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),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
child: const Icon(
|
|
|
|
|
|
Icons.person,
|
|
|
|
|
|
size: 80,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// 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,
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 20:59:47 +05:30
|
|
|
|
// ── Status + Connect (single row matching web public view) ──
|
2026-03-08 00:41:44 +05:30
|
|
|
|
Widget _buildStatusButtons(AgentDetailState state) {
|
|
|
|
|
|
final agent = state.agent!;
|
|
|
|
|
|
final isAvailable = agent.isAvailable;
|
|
|
|
|
|
final connState = state.connectionState;
|
|
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 38),
|
2026-03-15 00:28:04 +05:30
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (mounted) setState(() => _isStartingConversation = false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildStartMessageButton(AgentProfile agent) {
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: _isStartingConversation ? null : () => _handleStartMessage(agent),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
child: Container(
|
2026-03-15 00:28:04 +05:30
|
|
|
|
height: 46,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
decoration: BoxDecoration(
|
2026-03-15 00:28:04 +05:30
|
|
|
|
color: AppColors.accentOrange,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
borderRadius: BorderRadius.circular(15),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
2026-03-15 00:28:04 +05:30
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
child: _isStartingConversation
|
|
|
|
|
|
? const SizedBox(
|
|
|
|
|
|
width: 20,
|
|
|
|
|
|
height: 20,
|
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
|
color: Colors.white,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
),
|
2026-03-15 00:28:04 +05:30
|
|
|
|
)
|
|
|
|
|
|
: 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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildConnectButton(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
String connState,
|
|
|
|
|
|
bool isAvailable,
|
|
|
|
|
|
VoidCallback? onConnect,
|
|
|
|
|
|
) {
|
2026-03-08 00:41:44 +05:30
|
|
|
|
Color bgColor;
|
|
|
|
|
|
Color textColor;
|
|
|
|
|
|
String label;
|
|
|
|
|
|
bool enabled;
|
|
|
|
|
|
|
|
|
|
|
|
switch (connState) {
|
|
|
|
|
|
case 'pending':
|
2026-03-14 20:59:47 +05:30
|
|
|
|
bgColor = const Color(0xFFFEF3C7); // yellow-100
|
|
|
|
|
|
textColor = const Color(0xFF92600F);
|
2026-03-08 00:41:44 +05:30
|
|
|
|
label = 'Pending';
|
|
|
|
|
|
enabled = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'accepted':
|
2026-03-14 20:59:47 +05:30
|
|
|
|
bgColor = const Color(0xFFEF4444); // red-500
|
|
|
|
|
|
textColor = Colors.white;
|
2026-03-08 00:41:44 +05:30
|
|
|
|
label = 'Unlink';
|
|
|
|
|
|
enabled = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2026-03-14 20:59:47 +05:30
|
|
|
|
bgColor = isAvailable
|
|
|
|
|
|
? AppColors.accentOrange
|
|
|
|
|
|
: AppColors.primaryDark.withValues(alpha: 0.7);
|
|
|
|
|
|
textColor = Colors.white;
|
2026-03-08 00:41:44 +05:30
|
|
|
|
label = 'Connect';
|
2026-03-14 20:59:47 +05:30
|
|
|
|
enabled = isAvailable;
|
2026-03-08 00:41:44 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: enabled ? onConnect : null,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
height: 55,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
width: 140,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: bgColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(15),
|
|
|
|
|
|
),
|
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
label,
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontFamily: 'Fractul',
|
|
|
|
|
|
fontSize: 14,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
fontWeight: FontWeight.w600,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
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,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
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,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: () => Navigator.of(ctx).pop(),
|
|
|
|
|
|
child: Icon(
|
|
|
|
|
|
Icons.close,
|
|
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
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(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
|
|
|
|
width: 1,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
2026-03-15 00:03:38 +05:30
|
|
|
|
maxLines: 1,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
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: [
|
2026-03-14 20:59:47 +05:30
|
|
|
|
// Name + Verified badge + (Verified Expert) — badge after last name
|
2026-03-15 00:03:38 +05:30
|
|
|
|
Wrap(
|
|
|
|
|
|
alignment: WrapAlignment.center,
|
|
|
|
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
|
|
|
|
spacing: 4,
|
|
|
|
|
|
runSpacing: 4,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
2026-03-15 00:03:38 +05:30
|
|
|
|
'${agent.firstName} ${agent.lastName}',
|
2026-03-08 00:41:44 +05:30
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontFamily: 'Fractul',
|
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (agent.isVerified) ...[
|
2026-03-15 00:03:38 +05:30
|
|
|
|
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),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
// 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),
|
2026-03-16 14:23:31 +05:30
|
|
|
|
// Location (wrapping) + Member Since (separate line)
|
2026-03-14 20:59:47 +05:30
|
|
|
|
Builder(builder: (_) {
|
|
|
|
|
|
final parts = state.locationParts;
|
2026-03-16 14:23:31 +05:30
|
|
|
|
const maxVisible = 4;
|
|
|
|
|
|
final visibleParts = parts.take(maxVisible).join(', ');
|
2026-03-14 20:59:47 +05:30
|
|
|
|
final remaining = parts.length - maxVisible;
|
|
|
|
|
|
|
2026-03-16 14:23:31 +05:30
|
|
|
|
return Column(
|
2026-03-14 20:59:47 +05:30
|
|
|
|
children: [
|
|
|
|
|
|
if (parts.isNotEmpty) ...[
|
2026-03-16 14:23:31 +05:30
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
),
|
2026-03-16 14:23:31 +05:30
|
|
|
|
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,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontFamily: 'SourceSerif4',
|
2026-03-16 14:23:31 +05:30
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
fontWeight: FontWeight.w500,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
color: AppColors.primaryDark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-16 14:23:31 +05:30
|
|
|
|
],
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-14 20:59:47 +05:30
|
|
|
|
);
|
|
|
|
|
|
}),
|
|
|
|
|
|
// Expertise tags (matching web ProfileCard)
|
|
|
|
|
|
if (state.expertiseTags.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
|
Wrap(
|
|
|
|
|
|
alignment: WrapAlignment.center,
|
|
|
|
|
|
spacing: 8,
|
|
|
|
|
|
runSpacing: 8,
|
|
|
|
|
|
children: state.expertiseTags
|
|
|
|
|
|
.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(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Bio with Show More / Show Less
|
|
|
|
|
|
if (state.descriptionText.isNotEmpty) ...[
|
2026-03-08 00:41:44 +05:30
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 338,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
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,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
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),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
// Years (always show, matching web)
|
|
|
|
|
|
_buildExperienceItem('Years in Experience', [
|
|
|
|
|
|
state.yearsInExperience,
|
|
|
|
|
|
]),
|
|
|
|
|
|
// Contracts (always show, matching web)
|
|
|
|
|
|
_buildExperienceItem('Number of contracts closed', [
|
|
|
|
|
|
state.contractsClosed,
|
|
|
|
|
|
]),
|
2026-03-14 23:41:50 +05:30
|
|
|
|
// Licensing Areas (always show)
|
|
|
|
|
|
state.licensingAreas.isNotEmpty
|
|
|
|
|
|
? _buildLicensingAreas(state.licensingAreas)
|
|
|
|
|
|
: _buildEmptySection('Licensing & Areas', 'No licensed areas added'),
|
|
|
|
|
|
// Expertise Years (always show)
|
|
|
|
|
|
state.expertiseYears.isNotEmpty
|
|
|
|
|
|
? _buildExpertiseYears(state.expertiseYears)
|
|
|
|
|
|
: _buildEmptySection('Areas in expertise & Years', 'No expertise areas added'),
|
|
|
|
|
|
// Certifications (always show)
|
|
|
|
|
|
state.certifications.isNotEmpty
|
|
|
|
|
|
? _buildCertifications(state.certifications)
|
|
|
|
|
|
: _buildEmptySection('Certifications', 'No certifications added'),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
...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,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
2026-03-14 20:59:47 +05:30
|
|
|
|
item == '-' ? 'Not specified' : item,
|
|
|
|
|
|
style: TextStyle(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
fontWeight: FontWeight.w500,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
color: item == '-'
|
|
|
|
|
|
? AppColors.primaryDark.withValues(alpha: 0.4)
|
|
|
|
|
|
: AppColors.primaryDark,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
const SizedBox(height: 16),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
Divider(
|
|
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
|
|
|
|
height: 1,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 23:41:50 +05:30
|
|
|
|
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),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:41:44 +05:30
|
|
|
|
Widget _buildLicensingAreas(List<String> areas) {
|
|
|
|
|
|
return _ExpandableChipsSection(
|
|
|
|
|
|
title: 'Licensing & Areas',
|
|
|
|
|
|
items: areas,
|
|
|
|
|
|
initialCount: 6,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildExpertiseYears(List<Map<String, String>> items) {
|
|
|
|
|
|
final chips = items.map((e) {
|
|
|
|
|
|
final years = e['years'] ?? '';
|
|
|
|
|
|
return years.isNotEmpty ? '${e['area']} – $years' : e['area'] ?? '';
|
|
|
|
|
|
}).toList();
|
|
|
|
|
|
return _ExpandableChipsSection(
|
|
|
|
|
|
title: 'Areas in expertise & Years',
|
|
|
|
|
|
items: chips,
|
|
|
|
|
|
initialCount: 4,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildCertifications(List<Map<String, String>> certs) {
|
2026-03-14 17:25:33 +05:30
|
|
|
|
return _ExpandableCertificationsSection(certs: certs);
|
2026-03-08 00:41:44 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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();
|
2026-03-14 17:51:03 +05:30
|
|
|
|
if (slug.contains('work_environment') ||
|
|
|
|
|
|
slug.contains('preferred_environment')) {
|
2026-03-08 00:41:44 +05:30
|
|
|
|
if (fv.textValue != null) workEnv = fv.textValue!;
|
|
|
|
|
|
if (fv.jsonValue is List) {
|
|
|
|
|
|
workEnv = (fv.jsonValue as List).map((e) => e.toString()).join(', ');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-14 17:51:03 +05:30
|
|
|
|
if (slug.contains('best_experience') ||
|
|
|
|
|
|
slug.contains('amazing_experience')) {
|
2026-03-08 00:41:44 +05:30
|
|
|
|
if (fv.textValue != null) bestExp = fv.textValue!;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 36),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
2026-03-14 23:41:50 +05:30
|
|
|
|
// Availability card (always show)
|
|
|
|
|
|
_buildInfoCard(
|
|
|
|
|
|
icon: Icons.access_time_filled,
|
|
|
|
|
|
title: 'Availability',
|
|
|
|
|
|
subtitle: state.availabilityType.isNotEmpty
|
|
|
|
|
|
? state.availabilityType
|
|
|
|
|
|
: 'Not specified',
|
|
|
|
|
|
items: state.availabilitySchedule.isNotEmpty
|
|
|
|
|
|
? state.availabilitySchedule
|
|
|
|
|
|
: null,
|
|
|
|
|
|
isEmpty: state.availabilityType.isEmpty,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
// Work environment (always show)
|
|
|
|
|
|
_buildInfoCard(
|
|
|
|
|
|
icon: Icons.landscape_outlined,
|
|
|
|
|
|
title: 'Work Environment',
|
|
|
|
|
|
subtitle: '',
|
|
|
|
|
|
description: workEnv.isNotEmpty ? workEnv : 'Not specified',
|
|
|
|
|
|
isEmpty: workEnv.isEmpty,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
// Best experience (always show)
|
|
|
|
|
|
_buildInfoCard(
|
|
|
|
|
|
icon: Icons.star_rounded,
|
|
|
|
|
|
title: 'Best Experience',
|
|
|
|
|
|
subtitle: '',
|
|
|
|
|
|
description: bestExp.isNotEmpty ? bestExp : 'Not specified',
|
|
|
|
|
|
isEmpty: bestExp.isEmpty,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildInfoCard({
|
|
|
|
|
|
required IconData icon,
|
|
|
|
|
|
required String title,
|
|
|
|
|
|
required String subtitle,
|
|
|
|
|
|
List<String>? items,
|
|
|
|
|
|
String? description,
|
2026-03-14 23:41:50 +05:30
|
|
|
|
bool isEmpty = false,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
}) {
|
|
|
|
|
|
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,
|
2026-03-14 23:41:50 +05:30
|
|
|
|
style: TextStyle(
|
2026-03-08 00:41:44 +05:30
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
2026-03-14 23:41:50 +05:30
|
|
|
|
color: isEmpty
|
|
|
|
|
|
? AppColors.primaryDark.withValues(alpha: 0.4)
|
|
|
|
|
|
: AppColors.primaryDark,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
if (items != null && items.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(height: 8),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
...items.map(
|
|
|
|
|
|
(item) => Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 1),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
item,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
],
|
|
|
|
|
|
if (description != null) ...[
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
description,
|
2026-03-14 23:41:50 +05:30
|
|
|
|
style: TextStyle(
|
2026-03-08 00:41:44 +05:30
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
2026-03-14 23:41:50 +05:30
|
|
|
|
color: isEmpty
|
|
|
|
|
|
? AppColors.primaryDark.withValues(alpha: 0.4)
|
|
|
|
|
|
: AppColors.primaryDark,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
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),
|
2026-03-14 23:41:50 +05:30
|
|
|
|
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),
|
|
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
|
|
|
|
height: 1,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
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(
|
2026-03-14 21:30:37 +05:30
|
|
|
|
height: 380,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
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 _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: [
|
2026-03-14 17:51:03 +05:30
|
|
|
|
...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,
|
2026-03-08 02:01:03 +05:30
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
item,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
if (hasMore || _expanded)
|
2026-03-08 00:41:44 +05:30
|
|
|
|
GestureDetector(
|
2026-03-14 17:25:33 +05:30
|
|
|
|
onTap: () => setState(() => _expanded = !_expanded),
|
2026-03-08 02:01:03 +05:30
|
|
|
|
child: IntrinsicWidth(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
height: 28,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(15),
|
|
|
|
|
|
border: Border.all(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
color: _expanded
|
|
|
|
|
|
? AppColors.accentOrange
|
|
|
|
|
|
: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
|
|
|
|
width: 1,
|
|
|
|
|
|
),
|
2026-03-08 02:01:03 +05:30
|
|
|
|
),
|
2026-03-14 17:25:33 +05:30
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_expanded
|
|
|
|
|
|
? 'Show Less'
|
|
|
|
|
|
: '+${widget.items.length - widget.initialCount} More',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: _expanded ? 13 : 15,
|
2026-03-14 17:51:03 +05:30
|
|
|
|
fontWeight: _expanded
|
|
|
|
|
|
? FontWeight.w700
|
|
|
|
|
|
: FontWeight.w300,
|
2026-03-14 17:25:33 +05:30
|
|
|
|
color: _expanded
|
|
|
|
|
|
? AppColors.accentOrange
|
|
|
|
|
|
: AppColors.primaryDark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (_expanded) ...[
|
|
|
|
|
|
const SizedBox(width: 2),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
const Icon(
|
|
|
|
|
|
Icons.keyboard_arrow_up,
|
|
|
|
|
|
size: 14,
|
|
|
|
|
|
color: AppColors.accentOrange,
|
|
|
|
|
|
),
|
2026-03-14 17:25:33 +05:30
|
|
|
|
],
|
|
|
|
|
|
],
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 14),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
Divider(
|
|
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
|
|
|
|
height: 1,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 17:25:33 +05:30
|
|
|
|
// ── Expandable Certifications Section ──
|
|
|
|
|
|
|
|
|
|
|
|
class _ExpandableCertificationsSection extends StatefulWidget {
|
|
|
|
|
|
final List<Map<String, String>> certs;
|
|
|
|
|
|
final int initialCount;
|
|
|
|
|
|
|
|
|
|
|
|
const _ExpandableCertificationsSection({
|
|
|
|
|
|
required this.certs,
|
2026-03-14 21:30:37 +05:30
|
|
|
|
this.initialCount = 2,
|
2026-03-14 17:25:33 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@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: [
|
|
|
|
|
|
const Text(
|
|
|
|
|
|
'Certifications',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontFamily: 'Fractul',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
...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,
|
2026-03-14 17:25:33 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
),
|
|
|
|
|
|
Expanded(
|
2026-03-14 17:25:33 +05:30
|
|
|
|
child: Text(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
cert['name'] ?? '',
|
|
|
|
|
|
style: const TextStyle(
|
2026-03-14 17:25:33 +05:30
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 14,
|
2026-03-14 17:51:03 +05:30
|
|
|
|
color: AppColors.primaryDark,
|
2026-03-14 17:25:33 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
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),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
if (hasMore || _expanded)
|
2026-03-14 17:25:33 +05:30
|
|
|
|
Center(
|
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
|
onTap: () => setState(() => _expanded = !_expanded),
|
|
|
|
|
|
child: Container(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
horizontal: 14,
|
|
|
|
|
|
vertical: 4,
|
|
|
|
|
|
),
|
2026-03-14 17:25:33 +05:30
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(15),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
border: Border.all(color: AppColors.accentOrange, width: 1),
|
2026-03-14 17:25:33 +05:30
|
|
|
|
),
|
|
|
|
|
|
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),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:41:44 +05:30
|
|
|
|
// ── 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;
|
2026-03-08 02:01:03 +05:30
|
|
|
|
static const _initialCount = 3;
|
2026-03-08 00:41:44 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
constraints: const BoxConstraints(minHeight: 236),
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(15),
|
|
|
|
|
|
border: Border.all(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.7),
|
|
|
|
|
|
width: 0.7,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
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),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
...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,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
if (hasMore) ...[
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: () => setState(() => _expanded = !_expanded),
|
|
|
|
|
|
child: Container(
|
2026-03-14 17:51:03 +05:30
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
horizontal: 14,
|
|
|
|
|
|
vertical: 4,
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(15),
|
2026-03-14 17:51:03 +05:30
|
|
|
|
border: Border.all(color: AppColors.accentOrange, width: 1),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 21:30:37 +05:30
|
|
|
|
// ── Testimonial Card (matches Figma & web TestimonialCard) ──
|
2026-03-08 00:41:44 +05:30
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-03-14 21:30:37 +05:30
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
2026-03-14 21:30:37 +05:30
|
|
|
|
// 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
|
2026-03-08 00:41:44 +05:30
|
|
|
|
Expanded(
|
2026-03-14 21:30:37 +05:30
|
|
|
|
child: Text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontFamily: 'SourceSerif4',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w400,
|
|
|
|
|
|
color: AppColors.primaryDark,
|
|
|
|
|
|
height: 1.4,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
overflow: TextOverflow.fade,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
// Divider
|
|
|
|
|
|
Divider(
|
|
|
|
|
|
color: AppColors.primaryDark.withValues(alpha: 0.08),
|
|
|
|
|
|
height: 1,
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
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),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
),
|
2026-03-08 00:41:44 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-08 00:41:44 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 20:59:47 +05:30
|
|
|
|
// ── 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(
|
2026-03-14 23:46:22 +05:30
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
|
|
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
|
|
|
|
|
),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
|
),
|
2026-03-14 23:46:22 +05:30
|
|
|
|
child: SafeArea(
|
|
|
|
|
|
top: false,
|
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(24, 24, 24, 24),
|
|
|
|
|
|
child: _success ? _buildSuccessContent() : _buildFormContent(),
|
|
|
|
|
|
),
|
2026-03-14 21:30:37 +05:30
|
|
|
|
),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-03-14 23:46:22 +05:30
|
|
|
|
foregroundColor: Colors.white,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
disabledBackgroundColor:
|
|
|
|
|
|
AppColors.accentOrange.withValues(alpha: 0.5),
|
2026-03-14 23:46:22 +05:30
|
|
|
|
disabledForegroundColor: Colors.white,
|
2026-03-14 20:59:47 +05:30
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(24),
|
|
|
|
|
|
),
|
2026-03-14 23:46:22 +05:30
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
),
|
2026-03-14 23:46:22 +05:30
|
|
|
|
child: FittedBox(
|
|
|
|
|
|
fit: BoxFit.scaleDown,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_isSubmitting ? 'Sending...' : 'Send Request',
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontFamily: 'Fractul',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
),
|
2026-03-14 20:59:47 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|