refactor: replace team member carousel with dynamic team card grid in about screen

This commit is contained in:
pradeepkumar
2026-04-15 16:32:31 +05:30
parent 7b85d321af
commit 4bab9c06a7

View File

@@ -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,192 +596,144 @@ 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: [
Padding( if (state.teamTitle.isNotEmpty)
padding: const EdgeInsets.symmetric(horizontal: 57),
child: Text(
state.teamTitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 57),
child: Text(
state.teamSubtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 20),
// Team carousel
SizedBox(
height: 330,
child: PageView.builder(
itemCount: members.length,
controller: PageController(
viewportFraction: cardWidth / screenWidth,
),
onPageChanged: (index) {
ref.read(_aboutProvider.notifier).setActiveTeamIndex(index);
},
itemBuilder: (context, index) {
final member = members[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 0.1,
),
),
child: Column(
children: [
const SizedBox(height: 19),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: SizedBox(
height: 211,
width: double.infinity,
child: _buildTeamMemberImage(member.imageUrl, index),
),
),
),
const SizedBox(height: 16),
Text(
member.name,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
const SizedBox(height: 4),
Text(
member.role,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: Colors.black,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 16),
// Page indicator
if (members.length > 1)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 58), padding: const EdgeInsets.symmetric(horizontal: 57),
child: LayoutBuilder( child: Text(
builder: (context, constraints) { state.teamTitle,
final totalWidth = constraints.maxWidth; textAlign: TextAlign.center,
final segmentWidth = totalWidth / members.length; style: const TextStyle(
return Stack( fontFamily: 'Fractul',
children: [ fontSize: 25,
Container( fontWeight: FontWeight.w700,
height: 5, color: AppColors.primaryDark,
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,
borderRadius: BorderRadius.circular(15),
),
),
),
],
);
},
), ),
), ),
if (state.teamSubtitle.isNotEmpty) ...[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 57),
child: Text(
state.teamSubtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
],
if (state.teamIntro.isNotEmpty) ...[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
state.teamIntro,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
height: 1.5,
),
),
),
],
if (cards.isNotEmpty) ...[
const SizedBox(height: 24),
...cards.map(
(card) => Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 15),
child: _buildTeamCard(card),
),
),
],
], ],
); );
} }
// -- Team member image (handles asset://, S3 keys, and URLs) -- Widget _buildTeamCard(_AboutTeamCard card) {
// Falls back to local professional-N.jpg assets when CMS image fails. return Container(
Widget _buildTeamMemberImage(String imageUrl, int index) { width: double.infinity,
// Local fallback asset based on index padding: const EdgeInsets.all(24),
final fallbackAsset = 'assets/images/professional-${index + 1}.jpg'; decoration: BoxDecoration(
Widget fallbackImage() => Image.asset( borderRadius: BorderRadius.circular(15),
fallbackAsset, border: Border.all(
height: 211, color: AppColors.primaryDark.withValues(alpha: 0.1),
width: double.infinity, width: 1,
fit: BoxFit.cover, ),
errorBuilder: (_, __, ___) => const _TeamMemberPlaceholder(), ),
); child: Column(
children: [
if (imageUrl.isEmpty) return fallbackImage(); if (card.iconPath.isNotEmpty)
SizedBox(
// Local asset images (defaults) width: 36,
if (imageUrl.startsWith('asset://')) { height: 36,
final name = imageUrl.replaceFirst('asset://', ''); child: card.iconPath.startsWith('http')
return Image.asset( ? Image.network(
'assets/images/$name.jpg', card.iconPath,
height: 211, width: 36,
width: double.infinity, height: 36,
fit: BoxFit.cover, fit: BoxFit.contain,
errorBuilder: (_, __, ___) => fallbackImage(), errorBuilder: (_, __, ___) => const SizedBox.shrink(),
); )
} : S3Image(
imageUrl: card.iconPath,
// S3 key or URL from CMS — fall back to local asset on error width: 36,
return S3Image( height: 36,
imageUrl: imageUrl, fit: BoxFit.contain,
height: 211, placeholder: (_) => const SizedBox.shrink(),
fit: BoxFit.cover, errorWidget: (_) => const SizedBox.shrink(),
placeholder: (_) => fallbackImage(), ),
errorWidget: (_) => fallbackImage(), ),
if (card.title.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
card.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
],
if (card.description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
card.description,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
height: 1.5,
),
),
],
],
),
); );
} }
@@ -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),
),
);
}
}