feat: Add FAQ screen and navigation, implement authentication-gated access for home screen features, and dynamically render header menu options based on authentication status.
This commit is contained in:
4
assets/icons/home_faq_icon.svg
Normal file
4
assets/icons/home_faq_icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 9.5L11 3L19 9.5V18C19 18.5304 18.7893 19.0391 18.4142 19.4142C18.0391 19.7893 17.5304 20 17 20H5C4.46957 20 3.96086 19.7893 3.58579 19.4142C3.21071 19.0391 3 18.5304 3 18V9.5Z" stroke="#E58625" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 20V12H14V20" stroke="#E58625" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 491 B |
@@ -8,6 +8,7 @@ import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dar
|
|||||||
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
|
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
|
||||||
import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart';
|
import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart';
|
||||||
import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.dart';
|
import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.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';
|
import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart';
|
||||||
|
|
||||||
class AgentSearchScreen extends ConsumerStatefulWidget {
|
class AgentSearchScreen extends ConsumerStatefulWidget {
|
||||||
@@ -390,14 +391,26 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
|||||||
svgPath: 'assets/icons/nav_messages_icon.svg',
|
svgPath: 'assets/icons/nav_messages_icon.svg',
|
||||||
fallbackIcon: Icons.chat_bubble,
|
fallbackIcon: Icons.chat_bubble,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
final authState = ref.read(authProvider);
|
||||||
|
if (authState.status != AuthStatus.authenticated) {
|
||||||
|
context.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_buildNavItem(
|
_buildNavItem(
|
||||||
index: 3,
|
index: 3,
|
||||||
svgPath: 'assets/icons/nav_profile_icon.svg',
|
svgPath: 'assets/icons/nav_profile_icon.svg',
|
||||||
fallbackIcon: Icons.person_outline,
|
fallbackIcon: Icons.person_outline,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
final authState = ref.read(authProvider);
|
||||||
|
if (authState.status != AuthStatus.authenticated) {
|
||||||
|
context.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
545
lib/features/faq/presentation/screens/faq_screen.dart
Normal file
545
lib/features/faq/presentation/screens/faq_screen.dart
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
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/features/auth/presentation/providers/auth_provider.dart';
|
||||||
|
import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart';
|
||||||
|
|
||||||
|
// ── Data models ──
|
||||||
|
|
||||||
|
class FaqItem {
|
||||||
|
final int id;
|
||||||
|
final String icon;
|
||||||
|
final String question;
|
||||||
|
final String answer;
|
||||||
|
final String category;
|
||||||
|
|
||||||
|
const FaqItem({
|
||||||
|
required this.id,
|
||||||
|
required this.icon,
|
||||||
|
required this.question,
|
||||||
|
required this.answer,
|
||||||
|
required this.category,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FaqItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FaqItem(
|
||||||
|
id: json['id'] as int? ?? 0,
|
||||||
|
icon: json['icon'] as String? ?? '',
|
||||||
|
question: json['question'] as String? ?? '',
|
||||||
|
answer: json['answer'] as String? ?? '',
|
||||||
|
category: json['category'] as String? ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Default data (same as web) ──
|
||||||
|
|
||||||
|
const _defaultFaqs = <FaqItem>[
|
||||||
|
FaqItem(
|
||||||
|
id: 1,
|
||||||
|
icon: 'assets/icons/home_faq_icon.svg',
|
||||||
|
question: 'How do I update my listing status?',
|
||||||
|
answer:
|
||||||
|
'To update your listing status, log in to your Agent Dashboard. Locate the property in your "Active Listings" tab, click the three-dot menu on the right, and select "Update Status." You can change it to Pending, Sold, or Temporarily Off Market. Changes are reflected immediately.',
|
||||||
|
category: 'agent',
|
||||||
|
),
|
||||||
|
FaqItem(
|
||||||
|
id: 2,
|
||||||
|
icon: 'assets/icons/home_faq_icon.svg',
|
||||||
|
question: 'What is the pre-approval process?',
|
||||||
|
answer:
|
||||||
|
'The pre-approval process involves submitting your financial documents to a lender who will review your credit history, income, assets, and debts. Once approved, you\'ll receive a pre-approval letter stating how much you can borrow, which strengthens your position when making offers on properties.',
|
||||||
|
category: 'buying',
|
||||||
|
),
|
||||||
|
FaqItem(
|
||||||
|
id: 3,
|
||||||
|
icon: 'assets/icons/home_faq_icon.svg',
|
||||||
|
question: 'How do I schedule an open house?',
|
||||||
|
answer:
|
||||||
|
'To schedule an open house, navigate to your Agent Dashboard and select the property you want to showcase. Click on "Schedule Open House" and choose your preferred date and time.',
|
||||||
|
category: 'agent',
|
||||||
|
),
|
||||||
|
FaqItem(
|
||||||
|
id: 4,
|
||||||
|
icon: 'assets/icons/home_faq_icon.svg',
|
||||||
|
question: 'How do I reset my password?',
|
||||||
|
answer:
|
||||||
|
'To reset your password, click on the "Login" button and then select "Forgot Password." Enter your registered email address and we\'ll send you a password reset link. Follow the link to create a new password.',
|
||||||
|
category: 'platform',
|
||||||
|
),
|
||||||
|
FaqItem(
|
||||||
|
id: 5,
|
||||||
|
icon: 'assets/icons/home_faq_icon.svg',
|
||||||
|
question: 'How do I contact a listing agent directly?',
|
||||||
|
answer:
|
||||||
|
'You can contact a listing agent by visiting the property page and clicking the "Contact Agent" button. You\'ll be able to send a message directly to the agent through our secure messaging system.',
|
||||||
|
category: 'buying',
|
||||||
|
),
|
||||||
|
FaqItem(
|
||||||
|
id: 6,
|
||||||
|
icon: 'assets/icons/home_faq_icon.svg',
|
||||||
|
question: 'What if I find inaccurate data on a listing?',
|
||||||
|
answer:
|
||||||
|
'If you find inaccurate information on a listing, please use the "Report Issue" button on the property page. Describe the inaccuracy and our team will review and correct it within 24-48 hours.',
|
||||||
|
category: 'platform',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Provider ──
|
||||||
|
|
||||||
|
final faqProvider =
|
||||||
|
StateNotifierProvider.autoDispose<FaqNotifier, FaqState>((ref) {
|
||||||
|
return FaqNotifier();
|
||||||
|
});
|
||||||
|
|
||||||
|
class FaqState {
|
||||||
|
final List<FaqItem> faqs;
|
||||||
|
final bool isLoading;
|
||||||
|
final int? expandedId;
|
||||||
|
|
||||||
|
const FaqState({
|
||||||
|
this.faqs = const [],
|
||||||
|
this.isLoading = true,
|
||||||
|
this.expandedId,
|
||||||
|
});
|
||||||
|
|
||||||
|
FaqState copyWith({
|
||||||
|
List<FaqItem>? faqs,
|
||||||
|
bool? isLoading,
|
||||||
|
int? expandedId,
|
||||||
|
bool clearExpanded = false,
|
||||||
|
}) {
|
||||||
|
return FaqState(
|
||||||
|
faqs: faqs ?? this.faqs,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
expandedId: clearExpanded ? null : (expandedId ?? this.expandedId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FaqNotifier extends StateNotifier<FaqState> {
|
||||||
|
FaqNotifier() : super(const FaqState()) {
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
try {
|
||||||
|
final response = await ApiClient.instance.dio.get('/cms/page/faq');
|
||||||
|
final data = response.data['data'] as List<dynamic>?;
|
||||||
|
if (data != null) {
|
||||||
|
for (final record in data) {
|
||||||
|
final map = record as Map<String, dynamic>;
|
||||||
|
if (map['sectionKey'] == 'faqContent') {
|
||||||
|
final content = map['content'] as Map<String, dynamic>?;
|
||||||
|
if (content != null) {
|
||||||
|
final faqsList = content['faqs'] as List<dynamic>?;
|
||||||
|
if (faqsList != null && faqsList.isNotEmpty) {
|
||||||
|
final faqs = faqsList
|
||||||
|
.map((e) => FaqItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
state = state.copyWith(
|
||||||
|
faqs: faqs,
|
||||||
|
isLoading: false,
|
||||||
|
expandedId: faqs.first.id,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Fall through to defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use defaults
|
||||||
|
state = state.copyWith(
|
||||||
|
faqs: _defaultFaqs,
|
||||||
|
isLoading: false,
|
||||||
|
expandedId: _defaultFaqs.first.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleFaq(int id) {
|
||||||
|
if (state.expandedId == id) {
|
||||||
|
state = state.copyWith(clearExpanded: true);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(expandedId: id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Screen ──
|
||||||
|
|
||||||
|
class FaqScreen extends ConsumerWidget {
|
||||||
|
const FaqScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final state = ref.watch(faqProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: const HomeHeader(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: state.isLoading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppColors.accentOrange),
|
||||||
|
)
|
||||||
|
: ListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 20, 0, 30),
|
||||||
|
children: [
|
||||||
|
// Title
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Text(
|
||||||
|
'Frequently Asked Questions ?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// FAQ items
|
||||||
|
...state.faqs.map((faq) => _FaqAccordion(
|
||||||
|
faq: faq,
|
||||||
|
isExpanded: state.expandedId == faq.id,
|
||||||
|
onTap: () =>
|
||||||
|
ref.read(faqProvider.notifier).toggleFaq(faq.id),
|
||||||
|
)),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Still Need Help section
|
||||||
|
const _StillNeedHelpSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: _buildBottomNavBar(context, ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
svgPath: 'assets/icons/nav_home_icon.svg',
|
||||||
|
fallbackIcon: Icons.home,
|
||||||
|
isSelected: false,
|
||||||
|
onTap: () => context.go('/home'),
|
||||||
|
),
|
||||||
|
_buildNavItem(
|
||||||
|
context: context,
|
||||||
|
svgPath: 'assets/icons/nav_list_icon.svg',
|
||||||
|
fallbackIcon: Icons.list,
|
||||||
|
isSelected: false,
|
||||||
|
onTap: () => context.push('/agents/search'),
|
||||||
|
),
|
||||||
|
_buildNavItem(
|
||||||
|
context: context,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildNavItem(
|
||||||
|
context: context,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavItem({
|
||||||
|
required BuildContext context,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accordion item ──
|
||||||
|
|
||||||
|
class _FaqAccordion extends StatelessWidget {
|
||||||
|
final FaqItem faq;
|
||||||
|
final bool isExpanded;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _FaqAccordion({
|
||||||
|
required this.faq,
|
||||||
|
required this.isExpanded,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Question row
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Icon
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/icons/home_faq_icon.svg',
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
placeholderBuilder: (_) => const Icon(
|
||||||
|
Icons.home_outlined,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
|
||||||
|
// Question text
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
faq.question,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: isExpanded ? 'Fractul' : 'SourceSerif4',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight:
|
||||||
|
isExpanded ? FontWeight.w500 : FontWeight.w600,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Chevron
|
||||||
|
Icon(
|
||||||
|
isExpanded
|
||||||
|
? Icons.keyboard_arrow_down
|
||||||
|
: Icons.keyboard_arrow_right,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Answer (expanded)
|
||||||
|
if (isExpanded) ...[
|
||||||
|
// Divider
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Divider(
|
||||||
|
height: 1,
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(56, 14, 20, 18),
|
||||||
|
child: Text(
|
||||||
|
faq.answer,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Still Need Help section ──
|
||||||
|
|
||||||
|
class _StillNeedHelpSection extends StatelessWidget {
|
||||||
|
const _StillNeedHelpSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 24, 20, 24),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Left side: text
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Still Need Help ?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'Our Support Team Is Available\nMon-Fri, 9am-6pm IST',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Right side: buttons
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
// Start Live Chat
|
||||||
|
_buildSupportButton(
|
||||||
|
icon: Icons.chat_bubble_outline,
|
||||||
|
label: 'Start Live Chat',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Email Support
|
||||||
|
_buildSupportButton(
|
||||||
|
icon: Icons.email_outlined,
|
||||||
|
label: 'Email Support',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSupportButton({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 162,
|
||||||
|
height: 38,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: AppColors.primaryDark),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'SourceSerif4',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.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/features/auth/presentation/providers/auth_provider.dart';
|
||||||
import 'package:real_estate_mobile/features/home/presentation/widgets/featured_professionals_section.dart';
|
import 'package:real_estate_mobile/features/home/presentation/widgets/featured_professionals_section.dart';
|
||||||
import 'package:real_estate_mobile/features/home/presentation/widgets/features_section.dart';
|
import 'package:real_estate_mobile/features/home/presentation/widgets/features_section.dart';
|
||||||
import 'package:real_estate_mobile/features/home/presentation/widgets/hero_section.dart';
|
import 'package:real_estate_mobile/features/home/presentation/widgets/hero_section.dart';
|
||||||
@@ -10,14 +12,14 @@ import 'package:real_estate_mobile/features/home/presentation/widgets/testimonia
|
|||||||
import 'package:real_estate_mobile/features/home/presentation/widgets/top_professionals_section.dart';
|
import 'package:real_estate_mobile/features/home/presentation/widgets/top_professionals_section.dart';
|
||||||
import 'package:real_estate_mobile/features/home/presentation/widgets/trust_stats_section.dart';
|
import 'package:real_estate_mobile/features/home/presentation/widgets/trust_stats_section.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends ConsumerStatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HomeScreen> createState() => _HomeScreenState();
|
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -148,6 +150,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
context.push('/agents/search');
|
context.push('/agents/search');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (index == 2 || index == 3) {
|
||||||
|
final authState = ref.read(authProvider);
|
||||||
|
if (authState.status != AuthStatus.authenticated) {
|
||||||
|
context.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() => _selectedIndex = index);
|
setState(() => _selectedIndex = index);
|
||||||
},
|
},
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
|
|||||||
@@ -154,7 +154,10 @@ class _MenuDrawer extends ConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.help_outline,
|
icon: Icons.help_outline,
|
||||||
label: "FAQ's",
|
label: "FAQ's",
|
||||||
onTap: () => Navigator.pop(context),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.push('/faq');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
context,
|
context,
|
||||||
@@ -165,55 +168,128 @@ class _MenuDrawer extends ConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(color: AppColors.divider, height: 1),
|
const Divider(color: AppColors.divider, height: 1),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildMenuItem(
|
if (ref.watch(authProvider).status ==
|
||||||
context,
|
AuthStatus.authenticated) ...[
|
||||||
icon: Icons.person_outline,
|
_buildMenuItem(
|
||||||
label: 'Account Settings',
|
context,
|
||||||
onTap: () => Navigator.pop(context),
|
icon: Icons.person_outline,
|
||||||
),
|
label: 'Account Settings',
|
||||||
_buildMenuItem(
|
onTap: () => Navigator.pop(context),
|
||||||
context,
|
),
|
||||||
icon: Icons.notifications_outlined,
|
_buildMenuItem(
|
||||||
label: 'Notification Settings',
|
context,
|
||||||
onTap: () => Navigator.pop(context),
|
icon: Icons.notifications_outlined,
|
||||||
),
|
label: 'Notification Settings',
|
||||||
const SizedBox(height: 16),
|
onTap: () => Navigator.pop(context),
|
||||||
const Divider(color: AppColors.divider, height: 1),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
// Log Out button
|
const Divider(color: AppColors.divider, height: 1),
|
||||||
SizedBox(
|
const SizedBox(height: 24),
|
||||||
width: double.infinity,
|
// Log Out button
|
||||||
child: OutlinedButton.icon(
|
SizedBox(
|
||||||
onPressed: () {
|
width: double.infinity,
|
||||||
Navigator.pop(context);
|
child: OutlinedButton.icon(
|
||||||
ref.read(authProvider.notifier).logout();
|
onPressed: () {
|
||||||
},
|
Navigator.pop(context);
|
||||||
icon: const Icon(
|
ref.read(authProvider.notifier).logout();
|
||||||
Icons.logout,
|
},
|
||||||
color: AppColors.accentOrange,
|
icon: const Icon(
|
||||||
size: 20,
|
Icons.logout,
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
'Log Out',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'Fractul',
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: AppColors.accentOrange,
|
color: AppColors.accentOrange,
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
label: const Text(
|
||||||
style: OutlinedButton.styleFrom(
|
'Log Out',
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
style: TextStyle(
|
||||||
side: const BorderSide(
|
fontFamily: 'Fractul',
|
||||||
color: AppColors.accentOrange,
|
fontSize: 16,
|
||||||
width: 1.5,
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
style: OutlinedButton.styleFrom(
|
||||||
borderRadius: BorderRadius.circular(15),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
] else ...[
|
||||||
|
// Login button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.push('/login');
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.login,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'Log In',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.accentOrange,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Sign Up button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.push('/signup');
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.person_add_outlined,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'Sign Up',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Fractul',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:real_estate_mobile/features/auth/presentation/providers/auth_pro
|
|||||||
import 'package:real_estate_mobile/features/auth/presentation/screens/login_screen.dart';
|
import 'package:real_estate_mobile/features/auth/presentation/screens/login_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/auth/presentation/screens/signup_screen.dart';
|
import 'package:real_estate_mobile/features/auth/presentation/screens/signup_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_search_screen.dart';
|
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_search_screen.dart';
|
||||||
|
import 'package:real_estate_mobile/features/faq/presentation/screens/faq_screen.dart';
|
||||||
import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart';
|
import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart';
|
||||||
|
|
||||||
final routerProvider = Provider<GoRouter>((ref) {
|
final routerProvider = Provider<GoRouter>((ref) {
|
||||||
@@ -18,25 +19,35 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
authRefreshNotifier.dispose();
|
authRefreshNotifier.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Public routes accessible without authentication (matching web middleware)
|
||||||
|
const publicRoutes = ['/home', '/agents/search', '/faq'];
|
||||||
|
const authRoutes = ['/login', '/signup'];
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
initialLocation: '/login',
|
initialLocation: '/home',
|
||||||
refreshListenable: authRefreshNotifier,
|
refreshListenable: authRefreshNotifier,
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
final authState = ref.read(authProvider);
|
final authState = ref.read(authProvider);
|
||||||
final isAuthenticated = authState.status == AuthStatus.authenticated;
|
final isAuthenticated = authState.status == AuthStatus.authenticated;
|
||||||
final isLoading = authState.status == AuthStatus.loading;
|
final isLoading = authState.status == AuthStatus.loading;
|
||||||
|
|
||||||
final isAuthRoute = state.matchedLocation == '/login' ||
|
final location = state.matchedLocation;
|
||||||
state.matchedLocation == '/signup';
|
final isAuthRoute = authRoutes.contains(location);
|
||||||
|
final isPublicRoute = publicRoutes.any(
|
||||||
|
(route) => location == route || location.startsWith('$route/'),
|
||||||
|
);
|
||||||
|
|
||||||
// While checking auth status, don't redirect
|
// While checking auth status, don't redirect
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
|
|
||||||
// If authenticated and on auth page, go to home
|
// If authenticated and on auth page (login/signup), go to home
|
||||||
if (isAuthenticated && isAuthRoute) return '/home';
|
if (isAuthenticated && isAuthRoute) return '/home';
|
||||||
|
|
||||||
// If not authenticated and on protected page, go to login
|
// Public routes are always accessible
|
||||||
if (!isAuthenticated && !isAuthRoute) return '/login';
|
if (isPublicRoute || isAuthRoute) return null;
|
||||||
|
|
||||||
|
// Protected routes require authentication — redirect to login
|
||||||
|
if (!isAuthenticated) return '/login';
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -60,6 +71,10 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return AgentSearchScreen(initialQuery: query);
|
return AgentSearchScreen(initialQuery: query);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/faq',
|
||||||
|
builder: (context, state) => const FaqScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user