refactor: replace team member carousel with dynamic team card grid in about screen
This commit is contained in:
@@ -73,22 +73,22 @@ class _AboutFeatureItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AboutTeamMember {
|
class _AboutTeamCard {
|
||||||
final String name;
|
final String iconPath;
|
||||||
final String role;
|
final String title;
|
||||||
final String imageUrl;
|
final String description;
|
||||||
|
|
||||||
const _AboutTeamMember({
|
const _AboutTeamCard({
|
||||||
required this.name,
|
required this.iconPath,
|
||||||
required this.role,
|
required this.title,
|
||||||
required this.imageUrl,
|
required this.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _AboutTeamMember.fromJson(Map<String, dynamic> json) {
|
factory _AboutTeamCard.fromJson(Map<String, dynamic> json) {
|
||||||
return _AboutTeamMember(
|
return _AboutTeamCard(
|
||||||
name: json['name'] as String? ?? '',
|
iconPath: json['iconPath'] as String? ?? '',
|
||||||
role: json['role'] as String? ?? '',
|
title: json['title'] as String? ?? '',
|
||||||
imageUrl: json['imageUrl'] as String? ?? '',
|
description: json['description'] as String? ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,11 +143,7 @@ const _defaultFeatures = [
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const _defaultTeamMembers = [
|
const _defaultTeamCards = <_AboutTeamCard>[];
|
||||||
_AboutTeamMember(name: 'Andrew', role: 'Founder & CEO', imageUrl: 'asset://professional-1'),
|
|
||||||
_AboutTeamMember(name: 'Thomas', role: 'Co-Founder', imageUrl: 'asset://professional-2'),
|
|
||||||
_AboutTeamMember(name: 'Darren', role: 'Advisor', imageUrl: 'asset://professional-3'),
|
|
||||||
];
|
|
||||||
|
|
||||||
// ── State ──
|
// ── State ──
|
||||||
|
|
||||||
@@ -159,7 +155,8 @@ class _AboutState {
|
|||||||
final List<_AboutFeatureItem> features;
|
final List<_AboutFeatureItem> features;
|
||||||
final String teamTitle;
|
final String teamTitle;
|
||||||
final String teamSubtitle;
|
final String teamSubtitle;
|
||||||
final List<_AboutTeamMember> teamMembers;
|
final String teamIntro;
|
||||||
|
final List<_AboutTeamCard> teamCards;
|
||||||
final _AboutCta cta;
|
final _AboutCta cta;
|
||||||
final int activeTeamIndex;
|
final int activeTeamIndex;
|
||||||
|
|
||||||
@@ -172,7 +169,8 @@ class _AboutState {
|
|||||||
this.teamTitle = 'Meet the minds behind the platform',
|
this.teamTitle = 'Meet the minds behind the platform',
|
||||||
this.teamSubtitle =
|
this.teamSubtitle =
|
||||||
'We are a team of passionate innovators, real estate experts, and designers working together to redefine how people connect with trusted agents.',
|
'We are a team of passionate innovators, real estate experts, and designers working together to redefine how people connect with trusted agents.',
|
||||||
this.teamMembers = _defaultTeamMembers,
|
this.teamIntro = '',
|
||||||
|
this.teamCards = _defaultTeamCards,
|
||||||
this.cta = const _AboutCta(),
|
this.cta = const _AboutCta(),
|
||||||
this.activeTeamIndex = 0,
|
this.activeTeamIndex = 0,
|
||||||
});
|
});
|
||||||
@@ -185,7 +183,8 @@ class _AboutState {
|
|||||||
List<_AboutFeatureItem>? features,
|
List<_AboutFeatureItem>? features,
|
||||||
String? teamTitle,
|
String? teamTitle,
|
||||||
String? teamSubtitle,
|
String? teamSubtitle,
|
||||||
List<_AboutTeamMember>? teamMembers,
|
String? teamIntro,
|
||||||
|
List<_AboutTeamCard>? teamCards,
|
||||||
_AboutCta? cta,
|
_AboutCta? cta,
|
||||||
int? activeTeamIndex,
|
int? activeTeamIndex,
|
||||||
}) {
|
}) {
|
||||||
@@ -197,7 +196,8 @@ class _AboutState {
|
|||||||
features: features ?? this.features,
|
features: features ?? this.features,
|
||||||
teamTitle: teamTitle ?? this.teamTitle,
|
teamTitle: teamTitle ?? this.teamTitle,
|
||||||
teamSubtitle: teamSubtitle ?? this.teamSubtitle,
|
teamSubtitle: teamSubtitle ?? this.teamSubtitle,
|
||||||
teamMembers: teamMembers ?? this.teamMembers,
|
teamIntro: teamIntro ?? this.teamIntro,
|
||||||
|
teamCards: teamCards ?? this.teamCards,
|
||||||
cta: cta ?? this.cta,
|
cta: cta ?? this.cta,
|
||||||
activeTeamIndex: activeTeamIndex ?? this.activeTeamIndex,
|
activeTeamIndex: activeTeamIndex ?? this.activeTeamIndex,
|
||||||
);
|
);
|
||||||
@@ -252,13 +252,15 @@ class _AboutNotifier extends StateNotifier<_AboutState> {
|
|||||||
case 'team':
|
case 'team':
|
||||||
final title = content['title'] as String?;
|
final title = content['title'] as String?;
|
||||||
final subtitle = content['subtitle'] as String?;
|
final subtitle = content['subtitle'] as String?;
|
||||||
final membersList = content['members'] as List<dynamic>?;
|
final intro = content['intro'] as String?;
|
||||||
|
final cardsList = content['cards'] as List<dynamic>?;
|
||||||
if (title != null) state = state.copyWith(teamTitle: title);
|
if (title != null) state = state.copyWith(teamTitle: title);
|
||||||
if (subtitle != null) state = state.copyWith(teamSubtitle: subtitle);
|
if (subtitle != null) state = state.copyWith(teamSubtitle: subtitle);
|
||||||
if (membersList != null && membersList.isNotEmpty) {
|
if (intro != null) state = state.copyWith(teamIntro: intro);
|
||||||
|
if (cardsList != null && cardsList.isNotEmpty) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
teamMembers: membersList
|
teamCards: cardsList
|
||||||
.map((e) => _AboutTeamMember.fromJson(e as Map<String, dynamic>))
|
.map((e) => _AboutTeamCard.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -594,21 +596,22 @@ class AboutScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Team Section (dynamic with carousel) --
|
// -- Team Section (dynamic — matches web: title, subtitle, intro, icon cards) --
|
||||||
Widget _buildTeamSection(
|
Widget _buildTeamSection(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
_AboutState state,
|
_AboutState state,
|
||||||
) {
|
) {
|
||||||
final members = state.teamMembers;
|
final cards = state.teamCards;
|
||||||
if (members.isEmpty) return const SizedBox.shrink();
|
final hasContent = state.teamTitle.isNotEmpty ||
|
||||||
|
state.teamSubtitle.isNotEmpty ||
|
||||||
final activeIndex = state.activeTeamIndex.clamp(0, members.length - 1);
|
state.teamIntro.isNotEmpty ||
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
cards.isNotEmpty;
|
||||||
final cardWidth = screenWidth - (69 * 2);
|
if (!hasContent) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (state.teamTitle.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 57),
|
padding: const EdgeInsets.symmetric(horizontal: 57),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -622,6 +625,7 @@ class AboutScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (state.teamSubtitle.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 57),
|
padding: const EdgeInsets.symmetric(horizontal: 57),
|
||||||
@@ -636,150 +640,100 @@ class AboutScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
],
|
||||||
// Team carousel
|
if (state.teamIntro.isNotEmpty) ...[
|
||||||
SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 330,
|
Padding(
|
||||||
child: PageView.builder(
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
itemCount: members.length,
|
child: Text(
|
||||||
controller: PageController(
|
state.teamIntro,
|
||||||
viewportFraction: cardWidth / screenWidth,
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
height: 1.5,
|
||||||
),
|
),
|
||||||
onPageChanged: (index) {
|
),
|
||||||
ref.read(_aboutProvider.notifier).setActiveTeamIndex(index);
|
),
|
||||||
},
|
],
|
||||||
itemBuilder: (context, index) {
|
if (cards.isNotEmpty) ...[
|
||||||
final member = members[index];
|
const SizedBox(height: 24),
|
||||||
return Padding(
|
...cards.map(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
(card) => Padding(
|
||||||
child: Container(
|
padding: const EdgeInsets.fromLTRB(24, 0, 24, 15),
|
||||||
|
child: _buildTeamCard(card),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTeamCard(_AboutTeamCard card) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||||
width: 0.1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 19),
|
if (card.iconPath.isNotEmpty)
|
||||||
Padding(
|
SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
width: 36,
|
||||||
child: ClipRRect(
|
height: 36,
|
||||||
borderRadius: BorderRadius.circular(15),
|
child: card.iconPath.startsWith('http')
|
||||||
child: SizedBox(
|
? Image.network(
|
||||||
height: 211,
|
card.iconPath,
|
||||||
width: double.infinity,
|
width: 36,
|
||||||
child: _buildTeamMemberImage(member.imageUrl, index),
|
height: 36,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||||
|
)
|
||||||
|
: S3Image(
|
||||||
|
imageUrl: card.iconPath,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
placeholder: (_) => const SizedBox.shrink(),
|
||||||
|
errorWidget: (_) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (card.title.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
member.name,
|
card.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: Colors.black,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
],
|
||||||
|
if (card.description.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
member.role,
|
card.description,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w400,
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// Page indicator
|
|
||||||
if (members.length > 1)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 58),
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final totalWidth = constraints.maxWidth;
|
|
||||||
final segmentWidth = totalWidth / members.length;
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
border: Border.all(
|
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
|
||||||
width: 0.1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AnimatedPositioned(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
left: segmentWidth * activeIndex,
|
|
||||||
child: Container(
|
|
||||||
height: 5,
|
|
||||||
width: segmentWidth,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
borderRadius: BorderRadius.circular(15),
|
height: 1.5,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
],
|
||||||
}
|
),
|
||||||
|
|
||||||
// -- Team member image (handles asset://, S3 keys, and URLs) --
|
|
||||||
// Falls back to local professional-N.jpg assets when CMS image fails.
|
|
||||||
Widget _buildTeamMemberImage(String imageUrl, int index) {
|
|
||||||
// Local fallback asset based on index
|
|
||||||
final fallbackAsset = 'assets/images/professional-${index + 1}.jpg';
|
|
||||||
Widget fallbackImage() => Image.asset(
|
|
||||||
fallbackAsset,
|
|
||||||
height: 211,
|
|
||||||
width: double.infinity,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) => const _TeamMemberPlaceholder(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (imageUrl.isEmpty) return fallbackImage();
|
|
||||||
|
|
||||||
// Local asset images (defaults)
|
|
||||||
if (imageUrl.startsWith('asset://')) {
|
|
||||||
final name = imageUrl.replaceFirst('asset://', '');
|
|
||||||
return Image.asset(
|
|
||||||
'assets/images/$name.jpg',
|
|
||||||
height: 211,
|
|
||||||
width: double.infinity,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) => fallbackImage(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// S3 key or URL from CMS — fall back to local asset on error
|
|
||||||
return S3Image(
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
height: 211,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
placeholder: (_) => fallbackImage(),
|
|
||||||
errorWidget: (_) => fallbackImage(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,16 +846,3 @@ class AboutScreen extends ConsumerWidget {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TeamMemberPlaceholder extends StatelessWidget {
|
|
||||||
const _TeamMemberPlaceholder();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: const Color(0xFFC4D9D4),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(Icons.person, size: 60, color: AppColors.primaryDark),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user