Files
mobile-app/lib/features/faq/presentation/screens/faq_screen.dart

548 lines
17 KiB
Dart

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;
}
context.go('/messages');
},
),
_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;
}
context.push('/profile');
},
),
],
),
),
),
);
}
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,
),
),
],
),
),
);
}
}