feat: Implement About Us and Agent Home screens, update routing, and add new assets and plugins.
This commit is contained in:
970
lib/features/about/presentation/screens/about_screen.dart
Normal file
970
lib/features/about/presentation/screens/about_screen.dart
Normal file
@@ -0,0 +1,970 @@
|
||||
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/core/constants/app_colors.dart';
|
||||
import 'package:real_estate_mobile/core/network/api_client.dart';
|
||||
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart';
|
||||
|
||||
// ── Data models (matching web CMS types) ──
|
||||
|
||||
class _AboutHero {
|
||||
final String badge;
|
||||
final String headline;
|
||||
final String description;
|
||||
final String ctaButtonText;
|
||||
final String bannerImageUrl;
|
||||
final String bannerOverlayText;
|
||||
|
||||
const _AboutHero({
|
||||
this.badge = 'Our Mission',
|
||||
this.headline = 'Connecting You with Trusted Agents.',
|
||||
this.description =
|
||||
'We believe finding the right home shouldn\u2019t be complicated. Our platform bridges the gap between buyers, sellers, and verified real estate agents for a smooth and transparent experience.',
|
||||
this.ctaButtonText = 'Find Your Agent',
|
||||
this.bannerImageUrl = '',
|
||||
this.bannerOverlayText = 'Building the future of property.',
|
||||
});
|
||||
|
||||
factory _AboutHero.fromJson(Map<String, dynamic> json) {
|
||||
return _AboutHero(
|
||||
badge: json['badge'] as String? ?? 'Our Mission',
|
||||
headline: json['headline'] as String? ?? 'Connecting You with Trusted Agents.',
|
||||
description: json['description'] as String? ??
|
||||
'We believe finding the right home shouldn\u2019t be complicated. Our platform bridges the gap between buyers, sellers, and verified real estate agents for a smooth and transparent experience.',
|
||||
ctaButtonText: json['ctaButtonText'] as String? ?? 'Find Your Agent',
|
||||
bannerImageUrl: json['bannerImageUrl'] as String? ?? '',
|
||||
bannerOverlayText: json['bannerOverlayText'] as String? ?? 'Building the future of property.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AboutStatItem {
|
||||
final String value;
|
||||
final String label;
|
||||
|
||||
const _AboutStatItem({required this.value, required this.label});
|
||||
|
||||
factory _AboutStatItem.fromJson(Map<String, dynamic> json) {
|
||||
return _AboutStatItem(
|
||||
value: json['value'] as String? ?? '',
|
||||
label: json['label'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AboutFeatureItem {
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
const _AboutFeatureItem({
|
||||
required this.iconPath,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
factory _AboutFeatureItem.fromJson(Map<String, dynamic> json) {
|
||||
return _AboutFeatureItem(
|
||||
iconPath: json['iconPath'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AboutTeamMember {
|
||||
final String name;
|
||||
final String role;
|
||||
final String imageUrl;
|
||||
|
||||
const _AboutTeamMember({
|
||||
required this.name,
|
||||
required this.role,
|
||||
required this.imageUrl,
|
||||
});
|
||||
|
||||
factory _AboutTeamMember.fromJson(Map<String, dynamic> json) {
|
||||
return _AboutTeamMember(
|
||||
name: json['name'] as String? ?? '',
|
||||
role: json['role'] as String? ?? '',
|
||||
imageUrl: json['imageUrl'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AboutCta {
|
||||
final String title;
|
||||
final String description;
|
||||
final String buttonText;
|
||||
|
||||
const _AboutCta({
|
||||
this.title = 'Ready to find a agent',
|
||||
this.description =
|
||||
'Search and connect with verified real estate agents who match your needs, location, and goals.',
|
||||
this.buttonText = 'Find Agents Now',
|
||||
});
|
||||
|
||||
factory _AboutCta.fromJson(Map<String, dynamic> json) {
|
||||
return _AboutCta(
|
||||
title: json['title'] as String? ?? 'Ready to find a agent',
|
||||
description: json['description'] as String? ??
|
||||
'Search and connect with verified real estate agents who match your needs, location, and goals.',
|
||||
buttonText: json['buttonText'] as String? ?? 'Find Agents Now',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Defaults ──
|
||||
|
||||
const _defaultStats = [
|
||||
_AboutStatItem(value: '15k+', label: 'Verified Agents'),
|
||||
_AboutStatItem(value: '98%', label: 'Customer Satisfaction'),
|
||||
];
|
||||
|
||||
const _defaultFeatures = [
|
||||
_AboutFeatureItem(
|
||||
iconPath: '',
|
||||
title: 'Verified Agents',
|
||||
description:
|
||||
'All agents on our platform are carefully verified to ensure trust, proven expertise, and consistent service, helping users connect with reliable professionals and enjoy a smooth, confident, and transparent property journey',
|
||||
),
|
||||
_AboutFeatureItem(
|
||||
iconPath: '',
|
||||
title: 'Total Transparency',
|
||||
description:
|
||||
'We maintain complete transparency by clearly displaying agent details, reviews, and activity, allowing users to make informed decisions with confidence, clarity, and trust throughout their entire property journey.',
|
||||
),
|
||||
_AboutFeatureItem(
|
||||
iconPath: '',
|
||||
title: 'Simple Journey',
|
||||
description:
|
||||
'Our platform simplifies every step of the property process, from discovering the right agent to closing with confidence, making the experience smooth, fast, and stress-free for users at every stage.',
|
||||
),
|
||||
];
|
||||
|
||||
const _defaultTeamMembers = [
|
||||
_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 ──
|
||||
|
||||
class _AboutState {
|
||||
final bool isLoading;
|
||||
final _AboutHero hero;
|
||||
final List<_AboutStatItem> stats;
|
||||
final String featuresBadge;
|
||||
final List<_AboutFeatureItem> features;
|
||||
final String teamTitle;
|
||||
final String teamSubtitle;
|
||||
final List<_AboutTeamMember> teamMembers;
|
||||
final _AboutCta cta;
|
||||
final int activeTeamIndex;
|
||||
|
||||
const _AboutState({
|
||||
this.isLoading = true,
|
||||
this.hero = const _AboutHero(),
|
||||
this.stats = _defaultStats,
|
||||
this.featuresBadge = 'Why Choose Us',
|
||||
this.features = _defaultFeatures,
|
||||
this.teamTitle = 'Meet the minds behind the platform',
|
||||
this.teamSubtitle =
|
||||
'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.cta = const _AboutCta(),
|
||||
this.activeTeamIndex = 0,
|
||||
});
|
||||
|
||||
_AboutState copyWith({
|
||||
bool? isLoading,
|
||||
_AboutHero? hero,
|
||||
List<_AboutStatItem>? stats,
|
||||
String? featuresBadge,
|
||||
List<_AboutFeatureItem>? features,
|
||||
String? teamTitle,
|
||||
String? teamSubtitle,
|
||||
List<_AboutTeamMember>? teamMembers,
|
||||
_AboutCta? cta,
|
||||
int? activeTeamIndex,
|
||||
}) {
|
||||
return _AboutState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
hero: hero ?? this.hero,
|
||||
stats: stats ?? this.stats,
|
||||
featuresBadge: featuresBadge ?? this.featuresBadge,
|
||||
features: features ?? this.features,
|
||||
teamTitle: teamTitle ?? this.teamTitle,
|
||||
teamSubtitle: teamSubtitle ?? this.teamSubtitle,
|
||||
teamMembers: teamMembers ?? this.teamMembers,
|
||||
cta: cta ?? this.cta,
|
||||
activeTeamIndex: activeTeamIndex ?? this.activeTeamIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Provider ──
|
||||
|
||||
class _AboutNotifier extends StateNotifier<_AboutState> {
|
||||
_AboutNotifier() : super(const _AboutState()) {
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final response = await ApiClient.instance.dio.get('/cms/page/about');
|
||||
final data = response.data['data'] as List<dynamic>?;
|
||||
if (data != null) {
|
||||
for (final record in data) {
|
||||
final map = record as Map<String, dynamic>;
|
||||
final sectionKey = map['sectionKey'] as String?;
|
||||
final content = map['content'] as Map<String, dynamic>?;
|
||||
if (content == null) continue;
|
||||
|
||||
switch (sectionKey) {
|
||||
case 'hero':
|
||||
if (content['headline'] != null || content['description'] != null) {
|
||||
state = state.copyWith(hero: _AboutHero.fromJson(content));
|
||||
}
|
||||
case 'stats':
|
||||
final statsList = content['stats'] as List<dynamic>?;
|
||||
if (statsList != null && statsList.isNotEmpty) {
|
||||
state = state.copyWith(
|
||||
stats: statsList
|
||||
.map((e) => _AboutStatItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
case 'features':
|
||||
final badge = content['badge'] as String?;
|
||||
final featuresList = content['features'] as List<dynamic>?;
|
||||
if (badge != null) {
|
||||
state = state.copyWith(featuresBadge: badge);
|
||||
}
|
||||
if (featuresList != null && featuresList.isNotEmpty) {
|
||||
state = state.copyWith(
|
||||
features: featuresList
|
||||
.map((e) => _AboutFeatureItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
case 'team':
|
||||
final title = content['title'] as String?;
|
||||
final subtitle = content['subtitle'] as String?;
|
||||
final membersList = content['members'] as List<dynamic>?;
|
||||
if (title != null) state = state.copyWith(teamTitle: title);
|
||||
if (subtitle != null) state = state.copyWith(teamSubtitle: subtitle);
|
||||
if (membersList != null && membersList.isNotEmpty) {
|
||||
state = state.copyWith(
|
||||
teamMembers: membersList
|
||||
.map((e) => _AboutTeamMember.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
case 'cta':
|
||||
if (content['title'] != null || content['description'] != null) {
|
||||
state = state.copyWith(cta: _AboutCta.fromJson(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall through to defaults
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: false);
|
||||
}
|
||||
|
||||
void setActiveTeamIndex(int index) {
|
||||
state = state.copyWith(activeTeamIndex: index);
|
||||
}
|
||||
}
|
||||
|
||||
final _aboutProvider =
|
||||
StateNotifierProvider.autoDispose<_AboutNotifier, _AboutState>((ref) {
|
||||
return _AboutNotifier();
|
||||
});
|
||||
|
||||
// ── Feature icon mapping (fallback when CMS has no iconPath) ──
|
||||
|
||||
const _featureIconFallback = <String, IconData>{
|
||||
'Verified Agents': Icons.verified,
|
||||
'Total Transparency': Icons.lock_outline,
|
||||
'Simple Journey': Icons.check_circle_outline,
|
||||
};
|
||||
|
||||
// ── Screen ──
|
||||
|
||||
class AboutScreen extends ConsumerWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(_aboutProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
const HomeHeader(),
|
||||
Expanded(
|
||||
child: state.isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 18),
|
||||
_buildHeroTitle(state.hero.headline),
|
||||
const SizedBox(height: 16),
|
||||
_buildHeroSubtitle(state.hero.description),
|
||||
const SizedBox(height: 24),
|
||||
_buildBannerImage(state.hero.bannerImageUrl),
|
||||
const SizedBox(height: 10),
|
||||
_buildBannerCaption(state.hero.bannerOverlayText),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatsRow(state.stats),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle(state.featuresBadge),
|
||||
const SizedBox(height: 12),
|
||||
_buildWhyChooseUsSubtitle(),
|
||||
const SizedBox(height: 24),
|
||||
...state.features.map((f) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 15),
|
||||
child: _buildFeatureCard(f),
|
||||
)),
|
||||
const SizedBox(height: 25),
|
||||
_buildTeamSection(context, ref, state),
|
||||
const SizedBox(height: 40),
|
||||
_buildCtaSection(context, state.cta),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNavBar(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Hero Title --
|
||||
Widget _buildHeroTitle(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Hero Subtitle --
|
||||
Widget _buildHeroSubtitle(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Banner Image (from CMS or fallback to local asset) --
|
||||
Widget _buildBannerImage(String imageUrl) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 57),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: SizedBox(
|
||||
height: 295,
|
||||
width: double.infinity,
|
||||
child: imageUrl.isNotEmpty
|
||||
? S3Image(
|
||||
imageUrl: imageUrl,
|
||||
height: 295,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context) => Image.asset(
|
||||
'assets/images/about_cityscape.png',
|
||||
height: 295,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Image.asset(
|
||||
'assets/images/about_cityscape.png',
|
||||
height: 295,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Banner Caption --
|
||||
Widget _buildBannerCaption(String text) {
|
||||
return Center(
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Stats Row (dynamic) --
|
||||
Widget _buildStatsRow(List<_AboutStatItem> stats) {
|
||||
if (stats.isEmpty) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: stats.map((s) => _buildStatItem(s.value, s.label)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String value, String label) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// -- Section Title --
|
||||
Widget _buildSectionTitle(String text) {
|
||||
return Center(
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Why Choose Us Subtitle --
|
||||
Widget _buildWhyChooseUsSubtitle() {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 65),
|
||||
child: Text(
|
||||
'Our core principles guide every interaction, ensuring you feel confident and supported at every step.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Feature Card (dynamic with S3 icon support) --
|
||||
Widget _buildFeatureCard(_AboutFeatureItem feature) {
|
||||
final fallbackIcon = _featureIconFallback[feature.title] ?? Icons.star_outline;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 57),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||
width: 0.1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (feature.iconPath.isNotEmpty)
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: S3Image(
|
||||
imageUrl: feature.iconPath,
|
||||
width: 36,
|
||||
height: 36,
|
||||
fit: BoxFit.contain,
|
||||
errorWidget: (_) => Icon(
|
||||
fallbackIcon,
|
||||
size: 36,
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
fallbackIcon,
|
||||
size: 36,
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
feature.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
feature.description,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Team Section (dynamic with carousel) --
|
||||
Widget _buildTeamSection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
_AboutState state,
|
||||
) {
|
||||
final members = state.teamMembers;
|
||||
if (members.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final activeIndex = state.activeTeamIndex.clamp(0, members.length - 1);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final cardWidth = screenWidth - (69 * 2);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
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: 40),
|
||||
// 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
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: 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,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// -- Team member image (handles asset://, S3 keys, and URLs) --
|
||||
Widget _buildTeamMemberImage(String imageUrl) {
|
||||
const placeholder = _TeamMemberPlaceholder();
|
||||
|
||||
if (imageUrl.isEmpty) return placeholder;
|
||||
|
||||
// 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: (context, error, stackTrace) => placeholder,
|
||||
);
|
||||
}
|
||||
|
||||
// S3 key or URL from CMS
|
||||
return S3Image(
|
||||
imageUrl: imageUrl,
|
||||
height: 211,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_) => placeholder,
|
||||
errorWidget: (_) => placeholder,
|
||||
);
|
||||
}
|
||||
|
||||
// -- CTA Section (dynamic) --
|
||||
Widget _buildCtaSection(BuildContext context, _AboutCta cta) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
cta.title,
|
||||
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: 83),
|
||||
child: Text(
|
||||
cta.description,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Illustration
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(56),
|
||||
child: Image.asset(
|
||||
'assets/images/about_find_agent.jpg',
|
||||
height: 241,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Find Agents Now button
|
||||
GestureDetector(
|
||||
onTap: () => context.push('/agents/search'),
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 210,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accentOrange,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
cta.buttonText,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Get Help button
|
||||
GestureDetector(
|
||||
onTap: () => context.push('/contact'),
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 210,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'Get Help',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// -- Bottom Navigation Bar --
|
||||
Widget _buildBottomNavBar(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0xFFE8E8E8), width: 1),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
svgPath: 'assets/icons/nav_home_icon.svg',
|
||||
fallbackIcon: Icons.home,
|
||||
isSelected: false,
|
||||
onTap: () => context.go('/home'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
svgPath: 'assets/icons/nav_list_icon.svg',
|
||||
fallbackIcon: Icons.list,
|
||||
isSelected: false,
|
||||
onTap: () => context.push('/agents/search'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
svgPath: 'assets/icons/nav_messages_icon.svg',
|
||||
fallbackIcon: Icons.chat_bubble,
|
||||
isSelected: false,
|
||||
onTap: () {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
context.go('/messages');
|
||||
},
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
svgPath: 'assets/icons/nav_profile_icon.svg',
|
||||
fallbackIcon: Icons.person_outline,
|
||||
isSelected: false,
|
||||
onTap: () {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
context.push('/profile');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required String svgPath,
|
||||
required IconData fallbackIcon,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: SvgPicture.asset(
|
||||
svgPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: isSelected
|
||||
? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn)
|
||||
: const ColorFilter.mode(
|
||||
AppColors.accentOrange, BlendMode.srcIn),
|
||||
placeholderBuilder: (_) => Icon(
|
||||
fallbackIcon,
|
||||
size: 24,
|
||||
color: isSelected ? AppColors.primaryDark : AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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