3rd commit

This commit is contained in:
2025-10-17 16:03:59 +05:30
parent ee53254fe6
commit 6086ae249f
20 changed files with 1621 additions and 5 deletions

View File

@@ -0,0 +1,52 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../viewmodel/support_model.dart';
abstract class HelpSupportEvent {}
class LoadFAQ extends HelpSupportEvent {}
class OpenQuestion extends HelpSupportEvent {
final SupportItem item;
OpenQuestion(this.item);
}
class HelpSupportState {
final List<SupportItem> faqs;
final bool isLoading;
HelpSupportState({required this.faqs, required this.isLoading});
factory HelpSupportState.initial() => HelpSupportState(faqs: [], isLoading: false);
HelpSupportState copyWith({
List<SupportItem>? faqs,
bool? isLoading,
}) {
return HelpSupportState(
faqs: faqs ?? this.faqs,
isLoading: isLoading ?? this.isLoading,
);
}
}
class HelpSupportBloc extends Bloc<HelpSupportEvent, HelpSupportState> {
HelpSupportBloc() : super(HelpSupportState.initial()) {
on<LoadFAQ>(_onLoadFAQ);
}
Future<void> _onLoadFAQ(LoadFAQ event, Emitter<HelpSupportState> emit) async {
emit(state.copyWith(isLoading: true));
await Future.delayed(const Duration(milliseconds: 500));
final data = [
SupportItem("How to redeem passes?", "assets/icon/material-symbols_policy-rounded.png", "Guide"),
SupportItem("Refund Policy", "assets/icon/mingcute_information-fill.png", "Policy"),
SupportItem("Payment Issues", "assets/icon/solar_dollar-bold.png", "Billing and Payments"),
SupportItem("How to redeem passes?", "assets/icon/material-symbols_policy-rounded.png", "Guide"),
SupportItem("Refund Policy", "assets/icon/mingcute_information-fill.png", "Policy"),
SupportItem("Payment Issues", "assets/icon/solar_dollar-bold.png", "Billing and Payments"),
];
emit(state.copyWith(isLoading: false, faqs: data));
}
}

View File

@@ -0,0 +1,75 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class TicketEvent {}
class SubjectChanged extends TicketEvent {
final String subject;
SubjectChanged(this.subject);
}
class DescriptionChanged extends TicketEvent {
final String description;
DescriptionChanged(this.description);
}
class UploadFile extends TicketEvent {}
class SubmitTicket extends TicketEvent {}
class TicketState {
final String subject;
final String description;
final PlatformFile? selectedFile;
final bool isLoading;
const TicketState({
required this.subject,
required this.description,
required this.selectedFile,
required this.isLoading,
});
factory TicketState.initial() => const TicketState(
subject: '',
description: '',
selectedFile: null,
isLoading: false,
);
TicketState copyWith({
String? subject,
String? description,
PlatformFile? selectedFile,
bool? isLoading,
}) {
return TicketState(
subject: subject ?? this.subject,
description: description ?? this.description,
selectedFile: selectedFile ?? this.selectedFile,
isLoading: isLoading ?? this.isLoading,
);
}
}
class TicketBloc extends Bloc<TicketEvent, TicketState> {
TicketBloc() : super(TicketState.initial()) {
on<SubjectChanged>((event, emit) => emit(state.copyWith(subject: event.subject)));
on<DescriptionChanged>((event, emit) => emit(state.copyWith(description: event.description)));
on<UploadFile>(_onUploadFile);
on<SubmitTicket>(_onSubmitTicket);
}
Future<void> _onUploadFile(UploadFile event, Emitter<TicketState> emit) async {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.isNotEmpty) {
emit(state.copyWith(selectedFile: result.files.first));
}
}
Future<void> _onSubmitTicket(SubmitTicket event, Emitter<TicketState> emit) async {
emit(state.copyWith(isLoading: true));
await Future.delayed(const Duration(seconds: 2)); // mock API call
emit(state.copyWith(isLoading: false));
}
}

View File

@@ -0,0 +1,146 @@
import 'package:citycards_partner_flutter/support/view/support_form_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import '../blocs/help_support_bloc.dart';
class HelpSupportPage extends StatelessWidget {
const HelpSupportPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => HelpSupportBloc()..add(LoadFAQ()),
child: const _HelpSupportView(),
);
}
}
class _HelpSupportView extends StatelessWidget {
const _HelpSupportView();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar:
AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 8),
child: Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Color(0xFFF06969),
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
centerTitle: true,
title: Text("Help & Support",
style: GoogleFonts.poppins(fontWeight: FontWeight.w700, color: Colors.black)),
),
body: BlocBuilder<HelpSupportBloc, HelpSupportState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Get assistance with your CityCards experience",
style: GoogleFonts.poppins(color: Colors.black54)),
const SizedBox(height: 16),
Text("Common Questions",
style: GoogleFonts.poppins(fontWeight: FontWeight.w700, fontSize: 16)),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: state.faqs.length,
itemBuilder: (context, index) {
final faq = state.faqs[index];
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SupportFormPage()),
);
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(faq.title,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500, fontSize: 14)),
Row(
children: [
Image.asset(faq.icon, width: 18, color: const Color(0xffF95F62)),
const SizedBox(width: 6),
Text(faq.tag,
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.black54)),
],
)
],
),
),
);
},
),
),
ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SupportFormPage()),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50))),
child: Container(
width: double.infinity,
child: Center(
child: Text("Contact Support",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600, color: Colors.white)),
),
),
),
const SizedBox(height: 10),
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
),
child: Container(
width: double.infinity,
child: Center(
child: Text("Browse FAQ",
style: GoogleFonts.poppins(
color: const Color(0xffF95F62),
fontWeight: FontWeight.w600)),
),
),
)
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,214 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import '../blocs/ticket_bloc.dart';
class SupportFormPage extends StatelessWidget {
const SupportFormPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TicketBloc(),
child: const _SupportFormView(),
);
}
}
class _SupportFormView extends StatelessWidget {
const _SupportFormView();
@override
Widget build(BuildContext context) {
final bloc = context.read<TicketBloc>();
return Scaffold(
backgroundColor: Colors.white,
bottomNavigationBar: BlocBuilder<TicketBloc, TicketState>(
builder: (context, state) {return
Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0.0,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: state.isLoading
? null
: () => bloc.add(SubmitTicket()),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
child: state.isLoading
? const CircularProgressIndicator(color: Colors.white)
: Text("Submit Ticket",
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.w600)),
),
),
),
);
}),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 8),
child: Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Color(0xFFF06969),
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
centerTitle: true,
title: Text("Support",
style: GoogleFonts.poppins(fontWeight: FontWeight.w700, color: Colors.black)),
),
body: BlocBuilder<TicketBloc, TicketState>(
builder: (context, state) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Need help? Were here for you. Raise a ticket and our support team will get back to you shortly",
style: GoogleFonts.poppins(color: Colors.black54, fontSize: 13),
),
const SizedBox(height: 20),
Text("Subject", style: GoogleFonts.poppins(fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
TextField(
onChanged: (v) => bloc.add(SubjectChanged(v)),
decoration: InputDecoration(
fillColor: Colors.black.withOpacity(0.04),
hintText: "Enter Subject",
filled: true,
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
Text("Description", style: GoogleFonts.poppins(fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
TextField(
onChanged: (v) => bloc.add(DescriptionChanged(v)),
maxLines: 4,
decoration: InputDecoration(
fillColor: Colors.black.withOpacity(0.04),
hintText: "Enter Description",
filled: true,
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
Text("File upload", style: GoogleFonts.poppins(fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
GestureDetector(
onTap: () => bloc.add(UploadFile()),
child: Container(
height: 45,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.04),
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.selectedFile != null
? state.selectedFile!.name
: "Upload File",
style: GoogleFonts.poppins(
color: state.selectedFile != null
? Colors.black
: Colors.black54,
),
),
),
const Icon(Icons.upload_outlined, color: Colors.black54),
],
),
),
),
const SizedBox(height: 30),
Text("Contact Details",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w700, fontSize: 15)),
const SizedBox(height: 12),
Divider(),
Row(
children: [
Expanded(
flex: 1,
child: Row(
children: [
const Icon(Icons.email_outlined, color: Colors.black87),
const SizedBox(width: 12),
Text("Email", style: GoogleFonts.poppins(fontSize: 13)),
],
),
),
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(width: 12),
Text("Lila Hart", style: GoogleFonts.poppins(fontSize: 13,color: Colors.black)),
],
),
),
],
),
Divider(),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 1,
child: Row(
children: [
const Icon(Icons.phone_outlined, color: Colors.black87),
const SizedBox(width: 12),
Text("Phone", style: GoogleFonts.poppins(fontSize: 13)),
],
),
),
Expanded(
flex: 2,
child: Row(
children: [
const SizedBox(width: 12),
Text("(+971) 050 4245 564",
style: GoogleFonts.poppins(fontSize: 13)),
],
),
),
],
),
Divider(),
const SizedBox(height: 80),
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,7 @@
class SupportItem {
final String title;
final String icon;
final String tag;
SupportItem(this.title, this.icon, this.tag);
}