feat: Implement About Us and Agent Home screens, update routing, and add new assets and plugins.
This commit is contained in:
4
assets/icons/edit_icon.svg
Normal file
4
assets/icons/edit_icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 8H8C7.46957 8 6.96086 8.21071 6.58579 8.58579C6.21071 8.96086 6 9.46957 6 10V19C6 19.5304 6.21071 20.0391 6.58579 20.4142C6.96086 20.7893 7.46957 21 8 21H17C17.5304 21 18.0391 20.7893 18.4142 20.4142C18.7893 20.0391 19 19.5304 19 19V18" stroke="#E58625" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M18 6.00011L21 9.00011M22.385 7.58511C22.7788 7.19126 23.0001 6.65709 23.0001 6.10011C23.0001 5.54312 22.7788 5.00895 22.385 4.61511C21.9912 4.22126 21.457 4 20.9 4C20.343 4 19.8088 4.22126 19.415 4.61511L11 13.0001V16.0001H14L22.385 7.58511Z" stroke="#E58625" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 771 B |
BIN
assets/images/about_cityscape.png
Normal file
BIN
assets/images/about_cityscape.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/images/about_find_agent.jpg
Normal file
BIN
assets/images/about_find_agent.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
BIN
assets/images/about_team_andrew.jpg
Normal file
BIN
assets/images/about_team_andrew.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1236
lib/features/agents/presentation/screens/agent_home_screen.dart
Normal file
1236
lib/features/agents/presentation/screens/agent_home_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -148,7 +148,10 @@ class _MenuDrawer extends ConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.info_outline,
|
icon: Icons.info_outline,
|
||||||
label: 'About Us',
|
label: 'About Us',
|
||||||
onTap: () => Navigator.pop(context),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.push('/about');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:real_estate_mobile/config/app_config.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/constants/app_colors.dart';
|
||||||
|
import 'package:real_estate_mobile/core/utils/image_url_resolver.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/auth/presentation/providers/auth_provider.dart';
|
||||||
import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart';
|
import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart';
|
||||||
import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart';
|
import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ChatScreen extends ConsumerStatefulWidget {
|
class ChatScreen extends ConsumerStatefulWidget {
|
||||||
final String conversationId;
|
final String conversationId;
|
||||||
@@ -102,10 +105,12 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
String _formatTime(String isoTimestamp) {
|
String _formatTime(String isoTimestamp) {
|
||||||
try {
|
try {
|
||||||
final dt = DateTime.parse(isoTimestamp).toLocal();
|
final dt = DateTime.parse(isoTimestamp).toLocal();
|
||||||
final hour =
|
final hour = dt.hour > 12
|
||||||
dt.hour > 12 ? dt.hour - 12 : (dt.hour == 0 ? 12 : dt.hour);
|
? dt.hour - 12
|
||||||
|
: (dt.hour == 0 ? 12 : dt.hour);
|
||||||
final minute = dt.minute.toString().padLeft(2, '0');
|
final minute = dt.minute.toString().padLeft(2, '0');
|
||||||
return '$hour.$minute';
|
final amPm = dt.hour >= 12 ? 'PM' : 'AM';
|
||||||
|
return '$hour:$minute $amPm';
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -156,6 +161,18 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
return '$base/$avatarKey';
|
return '$base/$avatarKey';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatFileSize(int? bytes) {
|
||||||
|
if (bytes == null || bytes == 0) return '';
|
||||||
|
if (bytes < 1024) return '$bytes B';
|
||||||
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isGiphyUrl(String text) {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
return trimmed.contains('giphy.com/') || trimmed.contains('giphy.gif');
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// BUILD
|
// BUILD
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -219,7 +236,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// HEADER - matches Figma: rounded border, back arrow, name, status, icons
|
// HEADER
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
Widget _buildHeader({
|
Widget _buildHeader({
|
||||||
@@ -319,7 +336,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// CHAT BODY (profile section + messages)
|
// CHAT BODY
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
Widget _buildChatBody({
|
Widget _buildChatBody({
|
||||||
@@ -372,7 +389,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PROFILE SECTION (large avatar, name, specialties)
|
// PROFILE SECTION
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
Widget _buildProfileSection({
|
Widget _buildProfileSection({
|
||||||
@@ -399,7 +416,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorWidget: (_, __, ___) =>
|
errorWidget: (_, e, s) =>
|
||||||
_AvatarFallback(letter: initials, size: 80),
|
_AvatarFallback(letter: initials, size: 80),
|
||||||
)
|
)
|
||||||
: _AvatarFallback(letter: initials, size: 80),
|
: _AvatarFallback(letter: initials, size: 80),
|
||||||
@@ -472,7 +489,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSpecialtiesRow(String headline) {
|
Widget _buildSpecialtiesRow(String headline) {
|
||||||
// Split by comma, pipe, or similar separators
|
|
||||||
final specialties = headline.split(RegExp(r'[,|;]'))
|
final specialties = headline.split(RegExp(r'[,|;]'))
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.where((s) => s.isNotEmpty)
|
.where((s) => s.isNotEmpty)
|
||||||
@@ -517,7 +533,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
required bool isTyping,
|
required bool isTyping,
|
||||||
required bool isLoadingMore,
|
required bool isLoadingMore,
|
||||||
}) {
|
}) {
|
||||||
// +1 for the profile section header at the end of reversed list
|
|
||||||
final itemCount =
|
final itemCount =
|
||||||
messages.length + 1 + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0);
|
messages.length + 1 + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0);
|
||||||
|
|
||||||
@@ -533,7 +548,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
|
|
||||||
final adjustedIndex = isTyping ? index - 1 : index;
|
final adjustedIndex = isTyping ? index - 1 : index;
|
||||||
|
|
||||||
// Profile section at the top (last index in reversed list)
|
|
||||||
final profileIndex = messages.length + (isLoadingMore ? 1 : 0);
|
final profileIndex = messages.length + (isLoadingMore ? 1 : 0);
|
||||||
if (adjustedIndex == profileIndex) {
|
if (adjustedIndex == profileIndex) {
|
||||||
return _buildProfileSection(
|
return _buildProfileSection(
|
||||||
@@ -570,7 +584,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
final showDateSeparator =
|
final showDateSeparator =
|
||||||
_shouldShowDateSeparator(messages, adjustedIndex);
|
_shouldShowDateSeparator(messages, adjustedIndex);
|
||||||
|
|
||||||
// Get sender info
|
|
||||||
final senderName = isMine
|
final senderName = isMine
|
||||||
? (message.sender.displayName.isNotEmpty
|
? (message.sender.displayName.isNotEmpty
|
||||||
? message.sender.displayName
|
? message.sender.displayName
|
||||||
@@ -634,9 +647,222 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Sent Message (right-aligned, with avatar on right) --
|
// ============================================================
|
||||||
|
// MESSAGE CONTENT (handles text, image, gif, file)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
Widget _buildMessageContent(ChatMessage message, {required bool isMine}) {
|
||||||
|
switch (message.messageType) {
|
||||||
|
case MessageType.image:
|
||||||
|
return _buildImageContent(message, isMine: isMine);
|
||||||
|
case MessageType.file:
|
||||||
|
return _buildFileContent(message);
|
||||||
|
case MessageType.system:
|
||||||
|
return _buildSystemContent(message);
|
||||||
|
case MessageType.text:
|
||||||
|
// Detect Giphy URLs sent as text and render as GIF image
|
||||||
|
if (_isGiphyUrl(message.content)) {
|
||||||
|
return _buildImageContent(message, isMine: isMine);
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
message.content,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
textAlign: isMine ? TextAlign.right : TextAlign.left,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Image / GIF content --
|
||||||
|
Widget _buildImageContent(ChatMessage message, {required bool isMine}) {
|
||||||
|
final imageUrl = message.fileUrl ?? message.content;
|
||||||
|
|
||||||
|
// GIF from Giphy (direct URL) or S3 image
|
||||||
|
final isDirectUrl = imageUrl.startsWith('http://') || imageUrl.startsWith('https://');
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _showImageFullScreen(context, imageUrl),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 250,
|
||||||
|
maxHeight: 250,
|
||||||
|
),
|
||||||
|
child: isDirectUrl
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => _buildImagePlaceholder(),
|
||||||
|
errorWidget: (context, url, error) => _buildImageError(),
|
||||||
|
)
|
||||||
|
: S3Image(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (_) => _buildImagePlaceholder(),
|
||||||
|
errorWidget: (_) => _buildImageError(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePlaceholder() {
|
||||||
|
return Container(
|
||||||
|
width: 200,
|
||||||
|
height: 150,
|
||||||
|
color: const Color(0xFFF0F5FC),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageError() {
|
||||||
|
return Container(
|
||||||
|
width: 200,
|
||||||
|
height: 150,
|
||||||
|
color: const Color(0xFFF0F5FC),
|
||||||
|
child: const Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.broken_image_outlined, size: 40, color: AppColors.hintText),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Image not available',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppColors.hintText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- File content --
|
||||||
|
Widget _buildFileContent(ChatMessage message) {
|
||||||
|
final fileName = message.fileName ?? 'File';
|
||||||
|
final fileSize = _formatFileSize(message.fileSize);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _openOrDownloadFile(message),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF0F5FC),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.accentOrange.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.attach_file,
|
||||||
|
size: 20,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
fileName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (fileSize.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
fileSize,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.hintText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(
|
||||||
|
Icons.download_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- System message --
|
||||||
|
Widget _buildSystemContent(ChatMessage message) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF0F5FC),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
message.content,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.hintText,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Message status indicator --
|
||||||
|
Widget _buildStatusIcon(MessageStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case MessageStatus.read:
|
||||||
|
return const Icon(Icons.done_all, size: 14, color: Color(0xFF2196F3));
|
||||||
|
case MessageStatus.delivered:
|
||||||
|
return Icon(Icons.done_all, size: 14, color: AppColors.hintText);
|
||||||
|
case MessageStatus.sent:
|
||||||
|
return Icon(Icons.done, size: 14, color: AppColors.hintText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SENT MESSAGE (right-aligned)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
Widget _buildSentMessage(ChatMessage message, String senderName) {
|
Widget _buildSentMessage(ChatMessage message, String senderName) {
|
||||||
final timestamp = _formatTime(message.createdAt);
|
final timestamp = _formatTime(message.createdAt);
|
||||||
|
final isMediaMessage = message.messageType == MessageType.image ||
|
||||||
|
message.messageType == MessageType.file;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -648,10 +874,12 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
// Name row with timestamp
|
// Name row with timestamp and status
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
_buildStatusIcon(message.status),
|
||||||
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
timestamp,
|
timestamp,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -693,8 +921,13 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
// Message text
|
// Message content
|
||||||
Text(
|
_buildMessageContent(message, isMine: true),
|
||||||
|
// Show text content below media if present
|
||||||
|
if (isMediaMessage && message.content.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
message.content,
|
message.content,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
@@ -704,6 +937,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
),
|
),
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -714,10 +948,15 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Received Message (left-aligned, with avatar on left) --
|
// ============================================================
|
||||||
|
// RECEIVED MESSAGE (left-aligned)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
Widget _buildReceivedMessage(
|
Widget _buildReceivedMessage(
|
||||||
ChatMessage message, String senderName, String? avatar) {
|
ChatMessage message, String senderName, String? avatar) {
|
||||||
final timestamp = _formatTime(message.createdAt);
|
final timestamp = _formatTime(message.createdAt);
|
||||||
|
final isMediaMessage = message.messageType == MessageType.image ||
|
||||||
|
message.messageType == MessageType.file;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@@ -776,8 +1015,13 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
// Message text
|
// Message content
|
||||||
Text(
|
_buildMessageContent(message, isMine: false),
|
||||||
|
// Show text content below media if present
|
||||||
|
if (isMediaMessage && message.content.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
message.content,
|
message.content,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
@@ -786,6 +1030,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -806,7 +1051,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorWidget: (_, __, ___) =>
|
errorWidget: (_, e, s) =>
|
||||||
_AvatarFallback(letter: initials, size: size),
|
_AvatarFallback(letter: initials, size: size),
|
||||||
)
|
)
|
||||||
: _AvatarFallback(letter: initials, size: size),
|
: _AvatarFallback(letter: initials, size: size),
|
||||||
@@ -854,7 +1099,83 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// INPUT AREA - matches Figma: + circle, text input, send, mic circle
|
// IMAGE FULL SCREEN VIEWER
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void _showImageFullScreen(BuildContext context, String imageUrl) {
|
||||||
|
final isDirectUrl = imageUrl.startsWith('http://') || imageUrl.startsWith('https://');
|
||||||
|
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
iconTheme: const IconThemeData(color: Colors.white),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: InteractiveViewer(
|
||||||
|
minScale: 0.5,
|
||||||
|
maxScale: 4.0,
|
||||||
|
child: isDirectUrl
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
placeholder: (context, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => const Icon(
|
||||||
|
Icons.broken_image,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: S3Image(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
placeholder: (_) => const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
errorWidget: (_) => const Icon(
|
||||||
|
Icons.broken_image,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// FILE OPEN / DOWNLOAD
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
Future<void> _openOrDownloadFile(ChatMessage message) async {
|
||||||
|
final fileUrl = message.fileUrl;
|
||||||
|
if (fileUrl == null || fileUrl.isEmpty) return;
|
||||||
|
|
||||||
|
String? url;
|
||||||
|
if (fileUrl.startsWith('http://') || fileUrl.startsWith('https://')) {
|
||||||
|
url = fileUrl;
|
||||||
|
} else {
|
||||||
|
// Resolve S3 key to presigned download URL
|
||||||
|
url = await ImageUrlResolver.instance.resolve(fileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// INPUT AREA
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
Widget _buildInputArea() {
|
Widget _buildInputArea() {
|
||||||
@@ -864,7 +1185,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Plus icon (plain, no border)
|
// Plus icon
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _showAttachmentOptions,
|
onTap: _showAttachmentOptions,
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
@@ -881,15 +1202,17 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
// Text input pill with send icon inside
|
// Text input pill with send icon inside
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 40,
|
height: 44,
|
||||||
|
margin: const EdgeInsets.only(left: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(22),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -913,8 +1236,8 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 18,
|
||||||
vertical: 8,
|
vertical: 10,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
enabledBorder: InputBorder.none,
|
enabledBorder: InputBorder.none,
|
||||||
@@ -927,7 +1250,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
onTap: _sendMessage,
|
onTap: _sendMessage,
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 14),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.send,
|
Icons.send,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
|||||||
@@ -63,15 +63,15 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
// -- Header with back arrow, search, icons --
|
// -- Header with back arrow, search, icons --
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 51,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark.withValues(alpha: 0.2),
|
||||||
width: 0.5,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -88,11 +88,11 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: EdgeInsets.symmetric(horizontal: 14),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.arrow_back_ios_new,
|
Icons.arrow_back_ios_new,
|
||||||
size: 17,
|
size: 16,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -106,7 +106,7 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
@@ -114,7 +114,7 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.hintText,
|
color: AppColors.hintText,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@@ -129,7 +129,7 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -139,11 +139,11 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => setState(() => _isSearchActive = true),
|
onTap: () => setState(() => _isSearchActive = true),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.search,
|
Icons.search,
|
||||||
size: 20,
|
size: 22,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -152,11 +152,11 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: const EdgeInsets.only(right: 12, left: 4),
|
padding: EdgeInsets.only(right: 14, left: 2),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.more_horiz,
|
Icons.more_horiz,
|
||||||
size: 20,
|
size: 22,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -173,7 +173,7 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
baseColor: const Color(0xFFE8E8E8),
|
baseColor: const Color(0xFFE8E8E8),
|
||||||
highlightColor: const Color(0xFFF5F5F5),
|
highlightColor: const Color(0xFFF5F5F5),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
itemBuilder: (_, index) => Padding(
|
itemBuilder: (_, index) => Padding(
|
||||||
@@ -181,14 +181,14 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 52,
|
width: 56,
|
||||||
height: 52,
|
height: 56,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 14),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -215,7 +215,7 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
width: 45,
|
width: 55,
|
||||||
height: 12,
|
height: 12,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -303,10 +303,13 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
itemCount: filtered.length,
|
itemCount: filtered.length,
|
||||||
separatorBuilder: (_, __) => Divider(
|
separatorBuilder: (context, index) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Divider(
|
||||||
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||||
height: 0.5,
|
height: 1,
|
||||||
thickness: 0.5,
|
thickness: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return _ConversationTile(
|
return _ConversationTile(
|
||||||
@@ -318,7 +321,6 @@ class _ConversationsScreenState extends ConsumerState<ConversationsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Conversation Tile Widget --
|
// -- Conversation Tile Widget --
|
||||||
@@ -350,6 +352,7 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
final avatarUrl = _resolveAvatarUrl(otherParty.avatar);
|
final avatarUrl = _resolveAvatarUrl(otherParty.avatar);
|
||||||
final lastMessage = conversation.lastMessageText ?? '';
|
final lastMessage = conversation.lastMessageText ?? '';
|
||||||
final timestamp = _formatTimestamp(conversation.lastMessageAt);
|
final timestamp = _formatTimestamp(conversation.lastMessageAt);
|
||||||
|
final unread = conversation.unreadCount;
|
||||||
final firstLetter = otherParty.name.isNotEmpty
|
final firstLetter = otherParty.name.isNotEmpty
|
||||||
? otherParty.name[0].toUpperCase()
|
? otherParty.name[0].toUpperCase()
|
||||||
: '?';
|
: '?';
|
||||||
@@ -357,32 +360,45 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Avatar with online indicator
|
// Avatar with online indicator
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
ClipOval(
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ClipOval(
|
||||||
child: avatarUrl.isNotEmpty
|
child: avatarUrl.isNotEmpty
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: avatarUrl,
|
imageUrl: avatarUrl,
|
||||||
width: 52,
|
width: 56,
|
||||||
height: 52,
|
height: 56,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, url) => _AvatarFallback(
|
placeholder: (context, url) => _AvatarFallback(
|
||||||
letter: firstLetter,
|
letter: firstLetter,
|
||||||
),
|
),
|
||||||
errorWidget: (_, url, err) => _AvatarFallback(
|
errorWidget: (context, url, err) =>
|
||||||
|
_AvatarFallback(
|
||||||
letter: firstLetter,
|
letter: firstLetter,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _AvatarFallback(letter: firstLetter),
|
: _AvatarFallback(letter: firstLetter),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (otherParty.isOnline)
|
if (otherParty.isOnline)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 1,
|
||||||
bottom: 0,
|
bottom: 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
@@ -398,31 +414,61 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 14),
|
||||||
// Name and last message
|
// Name and last message
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
// Name row with optional unread badge
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
otherParty.name,
|
otherParty.name,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight:
|
||||||
|
unread > 0 ? FontWeight.w700 : FontWeight.w600,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (unread > 0) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 7, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$unread',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
lastMessage,
|
lastMessage,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: AppColors.primaryDark,
|
color: unread > 0
|
||||||
|
? AppColors.primaryDark
|
||||||
|
: AppColors.hintText,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -430,15 +476,17 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
// Timestamp
|
// Timestamp
|
||||||
Text(
|
Text(
|
||||||
timestamp,
|
timestamp,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: AppColors.primaryDark,
|
color: unread > 0
|
||||||
|
? AppColors.primaryDark
|
||||||
|
: AppColors.hintText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -458,8 +506,8 @@ class _AvatarFallback extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 52,
|
width: 56,
|
||||||
height: 52,
|
height: 56,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Color(0xFFE8E8E8),
|
color: Color(0xFFE8E8E8),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
@@ -469,7 +517,7 @@ class _AvatarFallback extends StatelessWidget {
|
|||||||
letter,
|
letter,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 20,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.primaryDark,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
@@ -497,7 +545,7 @@ String _formatTimestamp(String? isoTimestamp) {
|
|||||||
return 'Now';
|
return 'Now';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same day: show time like "2 hours Ago"
|
// Same day: show relative time
|
||||||
if (messageDate == today) {
|
if (messageDate == today) {
|
||||||
if (diff.inHours >= 1) {
|
if (diff.inHours >= 1) {
|
||||||
return '${diff.inHours} hour${diff.inHours > 1 ? 's' : ''} Ago';
|
return '${diff.inHours} hour${diff.inHours > 1 ? 's' : ''} Ago';
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import 'package:real_estate_mobile/features/home/presentation/screens/home_scree
|
|||||||
import 'package:real_estate_mobile/features/messaging/presentation/screens/conversations_screen.dart';
|
import 'package:real_estate_mobile/features/messaging/presentation/screens/conversations_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/messaging/presentation/screens/chat_screen.dart';
|
import 'package:real_estate_mobile/features/messaging/presentation/screens/chat_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/messaging/presentation/screens/messaging_shell.dart';
|
import 'package:real_estate_mobile/features/messaging/presentation/screens/messaging_shell.dart';
|
||||||
|
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_home_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/contact/presentation/screens/contact_screen.dart';
|
import 'package:real_estate_mobile/features/contact/presentation/screens/contact_screen.dart';
|
||||||
|
import 'package:real_estate_mobile/features/about/presentation/screens/about_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart';
|
import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart';
|
||||||
|
|
||||||
final routerProvider = Provider<GoRouter>((ref) {
|
final routerProvider = Provider<GoRouter>((ref) {
|
||||||
@@ -26,7 +28,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Public routes accessible without authentication (matching web middleware)
|
// Public routes accessible without authentication (matching web middleware)
|
||||||
const publicRoutes = ['/home', '/agents/search', '/agents/detail', '/faq', '/contact'];
|
const publicRoutes = ['/home', '/agents/search', '/agents/detail', '/faq', '/contact', '/about'];
|
||||||
const authRoutes = ['/login', '/signup'];
|
const authRoutes = ['/login', '/signup'];
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
@@ -68,7 +70,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/home',
|
path: '/home',
|
||||||
builder: (context, state) => const HomeScreen(),
|
builder: (context, state) => const _HomeRouteWrapper(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/agents/search',
|
path: '/agents/search',
|
||||||
@@ -108,6 +110,10 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: '/contact',
|
path: '/contact',
|
||||||
builder: (context, state) => const ContactScreen(),
|
builder: (context, state) => const ContactScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/about',
|
||||||
|
builder: (context, state) => const AboutScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
builder: (context, state) => const ProfileSettingsScreen(),
|
builder: (context, state) => const ProfileSettingsScreen(),
|
||||||
@@ -116,6 +122,21 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Reactively switches between HomeScreen and AgentHomeScreen based on auth.
|
||||||
|
class _HomeRouteWrapper extends ConsumerWidget {
|
||||||
|
const _HomeRouteWrapper();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final authState = ref.watch(authProvider);
|
||||||
|
if (authState.status == AuthStatus.authenticated &&
|
||||||
|
authState.user?.role == 'AGENT') {
|
||||||
|
return const AgentHomeScreen();
|
||||||
|
}
|
||||||
|
return const HomeScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Notifier that triggers GoRouter refresh when auth state changes.
|
/// Notifier that triggers GoRouter refresh when auth state changes.
|
||||||
class _AuthRefreshNotifier extends ChangeNotifier {
|
class _AuthRefreshNotifier extends ChangeNotifier {
|
||||||
void notify() {
|
void notify() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
|
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
|
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
|
||||||
@@ -16,4 +17,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
emoji_picker_flutter
|
emoji_picker_flutter
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import flutter_secure_storage_macos
|
|||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
||||||
@@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
66
pubspec.lock
66
pubspec.lock
@@ -1037,6 +1037,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.28"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1151,4 +1215,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.1 <4.0.0"
|
dart: ">=3.10.1 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.38.0"
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ dependencies:
|
|||||||
# Pin to avoid objective_c crash on iOS 26 simulator
|
# Pin to avoid objective_c crash on iOS 26 simulator
|
||||||
path_provider_foundation: 2.4.0
|
path_provider_foundation: 2.4.0
|
||||||
|
|
||||||
|
# URL launching
|
||||||
|
url_launcher: ^6.3.1
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
logger: ^2.5.0
|
logger: ^2.5.0
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
|
|
||||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
|
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
|
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi"));
|
registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
emoji_picker_flutter
|
emoji_picker_flutter
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Reference in New Issue
Block a user