Files
CityCards_Customer_Flutter/lib/profile/view/faq/faq_view.dart

250 lines
8.9 KiB
Dart

import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/back_widget.dart';
import 'package:citycards_customer/common_packages/custom_expansion_tile.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart';
import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart';
import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart';
import '../../models/faq_n_privacy_n_terms_model.dart';
import '../../repository/faq_n_privacy_n_terms_repository.dart';
class FaqPage extends StatelessWidget {
const FaqPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => FAQnPrivacynTermsBloc(FAQnPrivacynTermsRepository())
..add(FetchFAQnPrivacynTermsEvent()),
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: BlocBuilder<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
builder: (context, state) {
if (state is FAQnPrivacynTermsLoading) {
return Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
if (state is FAQnPrivacynTermsError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${state.message}'),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () {
context
.read<FAQnPrivacynTermsBloc>()
.add(FetchFAQnPrivacynTermsEvent());
},
child: Text('Retry'),
),
],
),
);
}
if (state is FAQnPrivacynTermsLoaded) {
final faqs = state.data.faqs ?? [];
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: true,
showDivider: true,
),
backWidget(context, "FAQ", Colors.black),
SizedBox(height: 34.h),
...faqs.asMap().entries.map((entry) {
final categoryIndex = entry.key;
final category = entry.value;
// Only this section's expanded item index is passed down.
// If another category is open, this one gets -1 (all collapsed).
final expandedItemIndex =
state.expandedCategoryIndex == categoryIndex
? state.expandedItemIndex
: -1;
return Column(
children: [
FAQSection(
title: category.categoryName ?? '',
faqs: category.faqs ?? [],
categoryIndex: categoryIndex,
expandedItemIndex: expandedItemIndex,
),
if (categoryIndex < faqs.length - 1)
SizedBox(height: 20.h),
],
);
}).toList(),
],
),
),
);
}
return Center(child: Text('No data available'));
},
),
),
),
);
}
}
// StatefulWidget ONLY to hold ExpansibleControllers for smooth animation.
// All open/close logic still lives in BLoC — no setState anywhere.
class FAQSection extends StatefulWidget {
const FAQSection({
super.key,
required this.title,
required this.faqs,
required this.categoryIndex,
required this.expandedItemIndex,
});
final String title;
final List<FaqItem> faqs;
final int categoryIndex;
final int expandedItemIndex; // -1 = none open in this section
@override
State<FAQSection> createState() => _FAQSectionState();
}
class _FAQSectionState extends State<FAQSection> {
late List<ExpansibleController> _controllers;
// When we call expand()/collapse() programmatically, this flag is true.
// onExpansionChanged must ignore callbacks during that time, otherwise
// the programmatic expand fires another ToggleFAQItemEvent which
// flips the bloc state back — breaking the close-other behaviour.
bool _isProgrammatic = false;
@override
void initState() {
super.initState();
_controllers = List.generate(
widget.faqs.length,
(_) => ExpansibleController(),
);
}
@override
void didUpdateWidget(FAQSection oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.expandedItemIndex == widget.expandedItemIndex) return;
_isProgrammatic = true;
for (int i = 0; i < _controllers.length; i++) {
if (i == widget.expandedItemIndex) {
if (!_controllers[i].isExpanded) _controllers[i].expand();
} else {
if (_controllers[i].isExpanded) _controllers[i].collapse();
}
}
// Reset flag after current frame so user taps are handled normally again
WidgetsBinding.instance.addPostFrameCallback((_) {
_isProgrammatic = false;
});
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w),
decoration: BoxDecoration(
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(10.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: widget.title,
size: 16.sp,
weight: FontWeight.w500,
color: Color(0xFF212121),
),
SizedBox(height: 12.h),
Column(
children: widget.faqs.asMap().entries.map((entry) {
final index = entry.key;
final faq = entry.value;
return Column(
children: [
CustomExpansionTile(
key: ValueKey('cat_${widget.categoryIndex}_item_$index'),
controller: _controllers[index],
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut,
duration: const Duration(milliseconds: 350),
reverseDuration: const Duration(milliseconds: 250),
),
minTileHeight: 42.h,
borderRadius: BorderRadius.circular(5.r),
backgroundColor: Color(0xFFFEE7E7),
collapsedBackgroundColor: Color(0xFFFEE7E7),
tilePadding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 0),
childrenPadding:
EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.h),
title: Text(
faq.question ?? '',
style: TextStyle(fontSize: 14.sp),
),
onExpansionChanged: (_) {
// Ignore callbacks we triggered ourselves via controller
if (_isProgrammatic) return;
context.read<FAQnPrivacynTermsBloc>().add(
ToggleFAQItemEvent(
categoryIndex: widget.categoryIndex,
tappedIndex: index,
),
);
},
children: [
Text(
faq.answer ?? '',
style: TextStyle(
color: Color(0xFF5C5C5C),
fontSize: 14.sp,
),
),
],
),
if (index != widget.faqs.length - 1) SizedBox(height: 8.h),
],
);
}).toList(),
),
],
),
);
}
}