3rd commit
This commit is contained in:
91
lib/booking/blocs/booking_bloc.dart
Normal file
91
lib/booking/blocs/booking_bloc.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../models/booking_model.dart';
|
||||
import '../viewmodels/booking_viewmodel.dart';
|
||||
|
||||
abstract class BookingEvent {}
|
||||
|
||||
class LoadBookings extends BookingEvent {
|
||||
final DateTime month;
|
||||
LoadBookings(this.month);
|
||||
}
|
||||
|
||||
class SelectDate extends BookingEvent {
|
||||
final DateTime date;
|
||||
SelectDate(this.date);
|
||||
}
|
||||
|
||||
class ToggleSlotExpand extends BookingEvent {
|
||||
final int attractionIndex;
|
||||
ToggleSlotExpand(this.attractionIndex);
|
||||
}
|
||||
|
||||
class BookingState {
|
||||
final DateTime focusedMonth;
|
||||
final DateTime? selectedDate;
|
||||
final List<BookingDay> bookings;
|
||||
final bool isLoading;
|
||||
final List<int> expandedAttractions;
|
||||
|
||||
BookingState({
|
||||
required this.focusedMonth,
|
||||
required this.selectedDate,
|
||||
required this.bookings,
|
||||
required this.isLoading,
|
||||
required this.expandedAttractions,
|
||||
});
|
||||
|
||||
factory BookingState.initial() => BookingState(
|
||||
focusedMonth: DateTime.now(),
|
||||
selectedDate: null,
|
||||
bookings: [],
|
||||
isLoading: false,
|
||||
expandedAttractions: [],
|
||||
);
|
||||
|
||||
BookingState copyWith({
|
||||
DateTime? focusedMonth,
|
||||
DateTime? selectedDate,
|
||||
List<BookingDay>? bookings,
|
||||
bool? isLoading,
|
||||
List<int>? expandedAttractions,
|
||||
}) {
|
||||
return BookingState(
|
||||
focusedMonth: focusedMonth ?? this.focusedMonth,
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
bookings: bookings ?? this.bookings,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
expandedAttractions: expandedAttractions ?? this.expandedAttractions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookingBloc extends Bloc<BookingEvent, BookingState> {
|
||||
final BookingViewModel viewModel;
|
||||
|
||||
BookingBloc({required this.viewModel}) : super(BookingState.initial()) {
|
||||
on<LoadBookings>(_onLoadBookings);
|
||||
on<SelectDate>(_onSelectDate);
|
||||
on<ToggleSlotExpand>(_onToggleSlotExpand);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBookings(
|
||||
LoadBookings event, Emitter<BookingState> emit) async {
|
||||
emit(state.copyWith(isLoading: true, focusedMonth: event.month));
|
||||
final data = await viewModel.getBookings(event.month);
|
||||
emit(state.copyWith(isLoading: false, bookings: data));
|
||||
}
|
||||
|
||||
void _onSelectDate(SelectDate event, Emitter<BookingState> emit) {
|
||||
emit(state.copyWith(selectedDate: event.date));
|
||||
}
|
||||
|
||||
void _onToggleSlotExpand(ToggleSlotExpand event, Emitter<BookingState> emit) {
|
||||
final expanded = List<int>.from(state.expandedAttractions);
|
||||
if (expanded.contains(event.attractionIndex)) {
|
||||
expanded.remove(event.attractionIndex);
|
||||
} else {
|
||||
expanded.add(event.attractionIndex);
|
||||
}
|
||||
emit(state.copyWith(expandedAttractions: expanded));
|
||||
}
|
||||
}
|
||||
0
lib/booking/models/attraction_model.dart
Normal file
0
lib/booking/models/attraction_model.dart
Normal file
26
lib/booking/models/booking_model.dart
Normal file
26
lib/booking/models/booking_model.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
class BookingDay {
|
||||
final DateTime date;
|
||||
final List<Attraction> attractions;
|
||||
|
||||
BookingDay({required this.date, required this.attractions});
|
||||
}
|
||||
|
||||
class Attraction {
|
||||
final String name;
|
||||
final String colorHex;
|
||||
final List<TimeSlot> slots;
|
||||
|
||||
Attraction({
|
||||
required this.name,
|
||||
required this.colorHex,
|
||||
required this.slots,
|
||||
});
|
||||
}
|
||||
|
||||
class TimeSlot {
|
||||
final String time;
|
||||
final int booked;
|
||||
final int total;
|
||||
|
||||
TimeSlot({required this.time, required this.booked, required this.total});
|
||||
}
|
||||
75
lib/booking/repositories/booking_repository.dart
Normal file
75
lib/booking/repositories/booking_repository.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import '../models/booking_model.dart';
|
||||
|
||||
class BookingRepository {
|
||||
Future<List<BookingDay>> fetchBookings(DateTime month) async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
return [
|
||||
BookingDay(
|
||||
date: DateTime(month.year, month.month, 13),
|
||||
attractions: [
|
||||
Attraction(
|
||||
name: "The Enchanted Garden Adventure Park",
|
||||
colorHex: "#4CAF50",
|
||||
slots: [
|
||||
TimeSlot(time: "1:30pm – 5:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "5:30pm – 7:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "7:30pm – 9:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "9:30pm – 11:30pm", booked: 15, total: 50),
|
||||
],
|
||||
),Attraction(
|
||||
name: "The Enchanted Garden Adventure Park",
|
||||
colorHex: "#4CAF50",
|
||||
slots: [
|
||||
TimeSlot(time: "1:30pm – 5:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "5:30pm – 7:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "7:30pm – 9:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "9:30pm – 11:30pm", booked: 15, total: 50),
|
||||
],
|
||||
),Attraction(
|
||||
name: "The Enchanted Garden Adventure Park",
|
||||
colorHex: "#4CAF50",
|
||||
slots: [
|
||||
TimeSlot(time: "1:30pm – 5:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "5:30pm – 7:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "7:30pm – 9:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "9:30pm – 11:30pm", booked: 15, total: 50),
|
||||
],
|
||||
),Attraction(
|
||||
name: "The Enchanted Garden Adventure Park",
|
||||
colorHex: "#4CAF50",
|
||||
slots: [
|
||||
TimeSlot(time: "1:30pm – 5:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "5:30pm – 7:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "7:30pm – 9:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "9:30pm – 11:30pm", booked: 15, total: 50),
|
||||
],
|
||||
),
|
||||
Attraction(
|
||||
name: "Central City Museum",
|
||||
colorHex: "#F48FB1",
|
||||
slots: [
|
||||
TimeSlot(time: "1:30pm – 5:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "5:30pm – 7:30pm", booked: 15, total: 50),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
BookingDay(
|
||||
date: DateTime(month.year, month.month, 14),
|
||||
attractions: [
|
||||
Attraction(
|
||||
name: "Skyline Observatory",
|
||||
colorHex: "#BA68C8",
|
||||
slots: [
|
||||
TimeSlot(time: "1:30pm – 5:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "5:30pm – 7:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "7:30pm – 9:30pm", booked: 15, total: 50),
|
||||
TimeSlot(time: "9:30pm – 11:30pm", booked: 15, total: 50),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
12
lib/booking/viewmodels/booking_viewmodel.dart
Normal file
12
lib/booking/viewmodels/booking_viewmodel.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import '../repositories/booking_repository.dart';
|
||||
import '../models/booking_model.dart';
|
||||
|
||||
class BookingViewModel {
|
||||
final BookingRepository repository;
|
||||
|
||||
BookingViewModel({required this.repository});
|
||||
|
||||
Future<List<BookingDay>> getBookings(DateTime month) async {
|
||||
return await repository.fetchBookings(month);
|
||||
}
|
||||
}
|
||||
181
lib/booking/views/booking_bottom_sheet.dart
Normal file
181
lib/booking/views/booking_bottom_sheet.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/booking_model.dart';
|
||||
import '../blocs/booking_bloc.dart';
|
||||
|
||||
class BookingBottomSheet extends StatelessWidget {
|
||||
final DateTime date;
|
||||
final BookingDay booking;
|
||||
|
||||
const BookingBottomSheet({
|
||||
super.key,
|
||||
required this.date,
|
||||
required this.booking,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<BookingBloc>();
|
||||
final formattedDate = DateFormat('EEEE, MMMM d, yyyy').format(date);
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.6,
|
||||
maxChildSize: 0.6,
|
||||
builder: (context, scrollController) {
|
||||
return BlocBuilder<BookingBloc, BookingState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(formattedDate,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
Text(
|
||||
"${booking.attractions.length} attractions available",
|
||||
style: GoogleFonts.poppins(color: Colors.black54)),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: RawScrollbar(
|
||||
thumbColor: Color(0xffF95F62),
|
||||
trackColor: Color(0xffF9E7E1),
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
thickness: 10,
|
||||
radius: const Radius.circular(8),
|
||||
interactive: true,
|
||||
// thumbColor: const MaterialStatePropertyAll(Color(0xffF95F62)),
|
||||
controller: scrollController,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 20), child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: booking.attractions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final attraction = booking.attractions[index];
|
||||
final isExpanded = state.expandedAttractions.contains(index);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse("0x22${attraction.colorHex.substring(1)}")),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(
|
||||
"0xff${attraction.colorHex.substring(1)}")),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(attraction.name,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13)),
|
||||
],
|
||||
),
|
||||
const Icon(Icons.arrow_forward_ios, size: 14)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: List.generate(
|
||||
isExpanded
|
||||
? attraction.slots.length
|
||||
: (attraction.slots.length > 2
|
||||
? 2
|
||||
: attraction.slots.length),
|
||||
(i) => _slotCard(attraction.slots[i]),
|
||||
),
|
||||
),
|
||||
if (attraction.slots.length > 2)
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
bloc.add(ToggleSlotExpand(index)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
isExpanded
|
||||
? "Show less"
|
||||
: "+${attraction.slots.length - 2} more",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black54,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _slotCard(TimeSlot slot) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(slot.time,
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black87)),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text("${slot.booked}/${slot.total}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.black87)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
372
lib/booking/views/booking_page.dart
Normal file
372
lib/booking/views/booking_page.dart
Normal file
@@ -0,0 +1,372 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
import '../blocs/booking_bloc.dart';
|
||||
import '../repositories/booking_repository.dart';
|
||||
import '../viewmodels/booking_viewmodel.dart';
|
||||
import '../models/booking_model.dart';
|
||||
import 'booking_bottom_sheet.dart';
|
||||
|
||||
class BookingPage extends StatelessWidget {
|
||||
const BookingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => BookingBloc(
|
||||
viewModel: BookingViewModel(repository: BookingRepository()),
|
||||
)..add(LoadBookings(DateTime.now())),
|
||||
child: const _BookingView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BookingView extends StatelessWidget {
|
||||
const _BookingView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: BlocBuilder<BookingBloc, BookingState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<BookingBloc>();
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildMonthHeader(bloc, state),
|
||||
const SizedBox(height: 10),
|
||||
_buildCalendar(context, bloc, state),
|
||||
const SizedBox(height: 12),
|
||||
_buildAttractionLegend(),
|
||||
const SizedBox(height: 20),
|
||||
_buildRecurringButton(),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24.0),
|
||||
child: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
child: const Icon(Icons.arrow_back, color: Colors.white, size: 22),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Booking",
|
||||
style: GoogleFonts.poppins(fontSize: 32, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 25),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Easily schedule and manage your bookings anytime,\nanywhere. Fast, simple, and secure.",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.black,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthHeader(BookingBloc bloc, BookingState state) {
|
||||
final formatter = DateFormat("MMM yyyy");
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final prevMonth = DateTime(
|
||||
state.focusedMonth.year, state.focusedMonth.month - 1, 1);
|
||||
bloc.add(LoadBookings(prevMonth));
|
||||
},
|
||||
child: _navButton(Icons.keyboard_arrow_left_rounded),
|
||||
),
|
||||
Text(
|
||||
formatter.format(state.focusedMonth),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final nextMonth = DateTime(
|
||||
state.focusedMonth.year, state.focusedMonth.month + 1, 1);
|
||||
bloc.add(LoadBookings(nextMonth));
|
||||
},
|
||||
child: _navButton(Icons.keyboard_arrow_right_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _navButton(IconData icon) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffF6F6F6),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: Colors.black, size: 25),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendar(BuildContext context, BookingBloc bloc, BookingState state) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: TableCalendar(
|
||||
firstDay: DateTime(2020),
|
||||
lastDay: DateTime(2030),
|
||||
focusedDay: state.focusedMonth,
|
||||
calendarFormat: CalendarFormat.month,
|
||||
headerVisible: false,
|
||||
availableGestures: AvailableGestures.none,
|
||||
startingDayOfWeek: StartingDayOfWeek.monday,
|
||||
daysOfWeekStyle: DaysOfWeekStyle(
|
||||
weekdayStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
weekendStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
calendarStyle: CalendarStyle(
|
||||
outsideDaysVisible: false,
|
||||
markersMaxCount: 0,
|
||||
defaultTextStyle: GoogleFonts.poppins(color: Colors.black87,fontSize: 22),
|
||||
weekendTextStyle: GoogleFonts.poppins(color: Colors.black87),
|
||||
),
|
||||
eventLoader: (day) {
|
||||
return state.bookings
|
||||
.where((b) =>
|
||||
b.date.year == day.year &&
|
||||
b.date.month == day.month &&
|
||||
b.date.day == day.day)
|
||||
.toList();
|
||||
},
|
||||
calendarBuilders: CalendarBuilders(
|
||||
defaultBuilder: (context, date, _) {
|
||||
final bookings = state.bookings
|
||||
.where((b) =>
|
||||
b.date.year == date.year &&
|
||||
b.date.month == date.month &&
|
||||
b.date.day == date.day)
|
||||
.toList();
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (bookings.isNotEmpty) {
|
||||
bloc.add(SelectDate(date));
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BookingBottomSheet(
|
||||
date: date,
|
||||
booking: bookings.first,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (bookings.isNotEmpty && bookings.first.attractions.length > 3)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF95F62),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
"+${bookings.first.attractions.length - 3}",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"${date.day}",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (bookings.isNotEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
...List.generate(
|
||||
bookings.first.attractions.length > 3
|
||||
? 3
|
||||
: bookings.first.attractions.length,
|
||||
(i) => Container(
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 2),
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(
|
||||
"0xff${bookings.first.attractions[i].colorHex.substring(1)}")),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttractionLegend() {
|
||||
final legends = [
|
||||
{"name": "The Enchanted Garden", "color": "#4CAF50", "percent": "90%"},
|
||||
{"name": "Central City Museum", "color": "#F48FB1", "percent": "50%"},
|
||||
{"name": "Skyline Observatory", "color": "#BA68C8", "percent": "50%"},
|
||||
{"name": "Historic Downtown Walking", "color": "#2196F3", "percent": "30%"},
|
||||
{"name": "Scenic Riverfront Stroll", "color": "#3F51B5", "percent": "30%"},
|
||||
{"name": "Cultural Arts District Tour", "color": "#8BC34A", "percent": "40%"},
|
||||
];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF1EE),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Attraction Legend",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...legends.map((item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Color(int.parse("0xff${item["color"]!.substring(1)}")),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item["name"]!,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.group, color: Colors.teal, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
item["percent"]!,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecurringButton() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: Text(
|
||||
"Add Recurring Block",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
258
lib/booking/views/selected_time_slot_page.dart
Normal file
258
lib/booking/views/selected_time_slot_page.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
|
||||
|
||||
class SelectedTimeSlotPage extends StatelessWidget {
|
||||
const SelectedTimeSlotPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"Selected Time Slot",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Attraction Name Section
|
||||
_sectionHeader("Attraction Name", true),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF6F6F6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
"The Enchanted Garden",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Time Slots
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_sectionHeader("Time Slots Available", true),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xffF95F62)),
|
||||
),
|
||||
child: Text(
|
||||
"+ Add timing",
|
||||
style: GoogleFonts.poppins(
|
||||
color: const Color(0xffF95F62),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_slotTile("1:30pm - 5:30pm"),
|
||||
_slotTile("5:30pm - 7:30pm"),
|
||||
_slotTile("7:30pm - 10:30pm"),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Capacity Used
|
||||
_sectionHeader("Capacity Used", true),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF6F6F6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.group,
|
||||
size: 18, color: Colors.green),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"45/50",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
"Available",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.green,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: 45 / 50,
|
||||
backgroundColor: const Color(0xffF9E7E1),
|
||||
valueColor: const AlwaysStoppedAnimation(
|
||||
Color(0xffF95F62)),
|
||||
minHeight: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Date
|
||||
_sectionHeader("Date", true),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF6F6F6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today,
|
||||
size: 18, color: Colors.black87),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"Sept 15, 2025",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.black87),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Save button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: Text(
|
||||
"Save",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Remove button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
side: const BorderSide(color: Color(0xffF95F62), width: 1.5),
|
||||
),
|
||||
child: Text(
|
||||
"Remove",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionHeader(String title, bool editable) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
color: Colors.black),
|
||||
),
|
||||
if (editable)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 6),
|
||||
child: Icon(Icons.edit, color: Color(0xffF95F62), size: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _slotTile(String time) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF6F6F6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time, color: Color(0xffF95F62), size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
time,
|
||||
style: GoogleFonts.poppins(fontSize: 14, color: Colors.black87),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.edit_outlined, color: Colors.black54, size: 18),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../booking/views/booking_page.dart';
|
||||
import '../booking/views/selected_time_slot_page.dart';
|
||||
import '../login/views/forgot_password_page.dart';
|
||||
import '../login/views/login_page.dart';
|
||||
import '../login/views/otp_verification_page.dart';
|
||||
@@ -6,7 +8,7 @@ import '../login/views/reset_password_page.dart';
|
||||
import '../onboarding/views/onboarding_page.dart';
|
||||
import '../profile/views/profile_page.dart';
|
||||
import '../scan_history/views/scan_history_page.dart';
|
||||
// Import other screens here as needed
|
||||
import '../support/view/help_support_page.dart';
|
||||
|
||||
class AppRouter {
|
||||
static const String onboarding = '/onboarding';
|
||||
@@ -17,6 +19,10 @@ class AppRouter {
|
||||
static const String otpVerification = '/otp_verification';
|
||||
static const String resetPassword = '/reset_password';
|
||||
static const String profileScreen = '/profile_screen';
|
||||
static const String bookingPage = '/booking_page';
|
||||
static const String selectedTimeSlotPage = '/selected_time_slot_page';
|
||||
static const String helpSupportPage = '/help_support_page';
|
||||
|
||||
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
@@ -34,6 +40,12 @@ class AppRouter {
|
||||
return MaterialPageRoute(builder: (_) => const ResetPasswordPage());
|
||||
case profileScreen:
|
||||
return MaterialPageRoute(builder: (_) => const ProfileScreen());
|
||||
case selectedTimeSlotPage:
|
||||
return MaterialPageRoute(builder: (_) => const SelectedTimeSlotPage());
|
||||
case bookingPage:
|
||||
return MaterialPageRoute(builder: (_) => const BookingPage());
|
||||
case helpSupportPage:
|
||||
return MaterialPageRoute(builder: (_) => const HelpSupportPage());
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
|
||||
@@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'core/app_router.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
@@ -27,7 +25,7 @@ class MyApp extends StatelessWidget {
|
||||
Theme.of(context).textTheme,
|
||||
)
|
||||
),
|
||||
initialRoute: AppRouter.profileScreen,
|
||||
initialRoute: AppRouter.bookingPage,
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
);
|
||||
}
|
||||
|
||||
52
lib/support/blocs/help_support_bloc.dart
Normal file
52
lib/support/blocs/help_support_bloc.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
75
lib/support/blocs/ticket_bloc.dart
Normal file
75
lib/support/blocs/ticket_bloc.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
146
lib/support/view/help_support_page.dart
Normal file
146
lib/support/view/help_support_page.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
214
lib/support/view/support_form_page.dart
Normal file
214
lib/support/view/support_form_page.dart
Normal 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? We’re 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),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/support/viewmodel/support_model.dart
Normal file
7
lib/support/viewmodel/support_model.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
class SupportItem {
|
||||
final String title;
|
||||
final String icon;
|
||||
final String tag;
|
||||
|
||||
SupportItem(this.title, this.icon, this.tag);
|
||||
}
|
||||
Reference in New Issue
Block a user