250 lines
8.9 KiB
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(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |