feat: Implement S3 image handling with a new resolver and widget, update agent display, and configure storage base URL.

This commit is contained in:
pradeepkumar
2026-03-07 22:19:17 +05:30
parent aefe80253f
commit ee08e86449
11 changed files with 238 additions and 101 deletions

View File

@@ -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?,
);
}
}

View File

@@ -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(

View File

@@ -1,9 +1,8 @@
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: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/presentation/providers/agents_provider.dart';
@@ -288,28 +287,13 @@ class _FeaturedProfessionalsSectionState
// -- Image helpers --
Widget _buildCardImage(AgentProfile agent, int index) {
final resolvedUrl = _resolveImageUrl(agent.avatarUrl);
if (resolvedUrl != null) {
return CachedNetworkImage(
imageUrl: resolvedUrl,
width: double.infinity,
height: 192,
fit: BoxFit.cover,
placeholder: (context, url) => _buildImagePlaceholder(index),
errorWidget: (context, url, error) => _buildImagePlaceholder(index),
);
}
return _buildImagePlaceholder(index);
}
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;
return S3Image(
imageUrl: agent.avatarUrl,
width: double.infinity,
height: 192,
placeholder: (_) => _buildImagePlaceholder(index),
errorWidget: (_) => _buildImagePlaceholder(index),
);
}
Widget _buildImagePlaceholder(int index) {

View File

@@ -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/presentation/providers/agents_provider.dart';
import 'package:real_estate_mobile/features/home/data/models/landing_page_content.dart';
@@ -271,34 +270,15 @@ class _TopProfessionalsSectionState
);
}
/// Resolves an image URL: full URL as-is, relative prepend base URL.
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;
}
/// Builds the card image from AgentProfile avatar.
/// Builds the card image from AgentProfile avatar using presigned URL.
Widget _buildCardImage(AgentProfile agent, int index) {
final resolvedUrl = _resolveImageUrl(agent.avatarUrl);
if (resolvedUrl != null) {
return CachedNetworkImage(
imageUrl: resolvedUrl,
width: double.infinity,
height: 192,
fit: BoxFit.cover,
placeholder: (context, url) => _buildImagePlaceholder(index),
errorWidget: (context, url, error) => _buildImagePlaceholder(index),
);
}
return _buildImagePlaceholder(index);
return S3Image(
imageUrl: agent.avatarUrl,
width: double.infinity,
height: 192,
placeholder: (_) => _buildImagePlaceholder(index),
errorWidget: (_) => _buildImagePlaceholder(index),
);
}
Widget _buildImagePlaceholder(int index) {