feat: Implement S3 image handling with a new resolver and widget, update agent display, and configure storage base URL.
This commit is contained in:
@@ -22,6 +22,7 @@ class AgentProfile {
|
||||
final AgentType? agentType;
|
||||
final AgentUser? user;
|
||||
final List<AgentFieldValue> fieldValues;
|
||||
final String? createdAt;
|
||||
|
||||
const AgentProfile({
|
||||
required this.id,
|
||||
@@ -45,10 +46,26 @@ class AgentProfile {
|
||||
this.agentType,
|
||||
this.user,
|
||||
this.fieldValues = const [],
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName'.trim();
|
||||
|
||||
/// Format "Member Since March 2024" from createdAt date string.
|
||||
String get memberSinceText {
|
||||
if (createdAt == null || createdAt!.isEmpty) return 'Member Since 2024';
|
||||
try {
|
||||
final date = DateTime.parse(createdAt!);
|
||||
const months = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
];
|
||||
return 'Member Since ${months[date.month - 1]} ${date.year}';
|
||||
} catch (_) {
|
||||
return 'Member Since 2024';
|
||||
}
|
||||
}
|
||||
|
||||
/// Get avatar URL — check agent avatar first, then user avatar
|
||||
String? get avatarUrl => avatar ?? user?.avatar;
|
||||
|
||||
@@ -175,6 +192,7 @@ class AgentProfile {
|
||||
?.map((e) => AgentFieldValue.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
createdAt: json['createdAt'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
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/config/app_config.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/data/models/agent_type.dart';
|
||||
import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart';
|
||||
@@ -110,6 +109,8 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 10,
|
||||
@@ -408,16 +409,6 @@ class _AgentCard extends StatefulWidget {
|
||||
class _AgentCardState extends State<_AgentCard> {
|
||||
bool _bioExpanded = false;
|
||||
|
||||
String? _resolveImageUrl(String? imageUrl) {
|
||||
if (imageUrl == null || imageUrl.isEmpty) return null;
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
final baseUrl = AppConfig.apiBaseUrl;
|
||||
if (baseUrl.isNotEmpty) return '$baseUrl$imageUrl';
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final agent = widget.agent;
|
||||
@@ -432,6 +423,13 @@ class _AgentCardState extends State<_AgentCard> {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.primaryDark.withValues(alpha: 0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFD9D9D9).withValues(alpha: 0.5),
|
||||
offset: const Offset(0, 10),
|
||||
blurRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
@@ -440,7 +438,17 @@ class _AgentCardState extends State<_AgentCard> {
|
||||
// Cover image with match badge
|
||||
Stack(
|
||||
children: [
|
||||
_buildCoverImage(agent),
|
||||
S3Image(
|
||||
imageUrl: agent.avatarUrl,
|
||||
width: double.infinity,
|
||||
height: 265,
|
||||
errorWidget: (_) => Container(
|
||||
width: double.infinity,
|
||||
height: 265,
|
||||
color: AppColors.gradientStart,
|
||||
child: const Icon(Icons.person, size: 80, color: AppColors.primaryDark),
|
||||
),
|
||||
),
|
||||
if (matchPercent != null)
|
||||
Positioned(
|
||||
top: 0,
|
||||
@@ -534,7 +542,7 @@ class _AgentCardState extends State<_AgentCard> {
|
||||
|
||||
// Headline / title
|
||||
Text(
|
||||
agent.headline ?? agent.agentType?.name ?? 'Licensed Real Estate Agent',
|
||||
agent.agentType?.name ?? agent.headline ?? 'Professional',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 15,
|
||||
@@ -581,13 +589,16 @@ class _AgentCardState extends State<_AgentCard> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Member Since 2024',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryDark,
|
||||
Flexible(
|
||||
child: Text(
|
||||
agent.memberSinceText,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -652,31 +663,6 @@ class _AgentCardState extends State<_AgentCard> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverImage(AgentProfile agent) {
|
||||
final resolvedUrl = _resolveImageUrl(agent.avatarUrl);
|
||||
|
||||
if (resolvedUrl != null) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: resolvedUrl,
|
||||
width: double.infinity,
|
||||
height: 265,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => _buildPlaceholder(),
|
||||
errorWidget: (_, __, ___) => _buildPlaceholder(),
|
||||
);
|
||||
}
|
||||
return _buildPlaceholder();
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 265,
|
||||
color: AppColors.gradientStart,
|
||||
child:
|
||||
const Icon(Icons.person, size: 80, color: AppColors.primaryDark),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(String label) {
|
||||
return Container(
|
||||
|
||||
Reference in New Issue
Block a user