From 6086ae249f4c040a91f2eb32558f16b62d97eda7 Mon Sep 17 00:00:00 2001 From: "Anuj.Maurya" Date: Fri, 17 Oct 2025 16:03:59 +0530 Subject: [PATCH] 3rd commit --- .../icon/material-symbols_policy-rounded.png | Bin 0 -> 574 bytes .../icon/mingcute_information-fill.png | Bin 0 -> 512 bytes assets/support/icon/solar_dollar-bold.png | Bin 0 -> 680 bytes lib/booking/blocs/booking_bloc.dart | 91 +++++ lib/booking/models/attraction_model.dart | 0 lib/booking/models/booking_model.dart | 26 ++ .../repositories/booking_repository.dart | 75 ++++ lib/booking/viewmodels/booking_viewmodel.dart | 12 + lib/booking/views/booking_bottom_sheet.dart | 181 +++++++++ lib/booking/views/booking_page.dart | 372 ++++++++++++++++++ .../views/selected_time_slot_page.dart | 258 ++++++++++++ lib/core/app_router.dart | 14 +- lib/main.dart | 4 +- lib/support/blocs/help_support_bloc.dart | 52 +++ lib/support/blocs/ticket_bloc.dart | 75 ++++ lib/support/view/help_support_page.dart | 146 +++++++ lib/support/view/support_form_page.dart | 214 ++++++++++ lib/support/viewmodel/support_model.dart | 7 + pubspec.lock | 95 ++++- pubspec.yaml | 4 + 20 files changed, 1621 insertions(+), 5 deletions(-) create mode 100644 assets/support/icon/material-symbols_policy-rounded.png create mode 100644 assets/support/icon/mingcute_information-fill.png create mode 100644 assets/support/icon/solar_dollar-bold.png create mode 100644 lib/booking/blocs/booking_bloc.dart create mode 100644 lib/booking/models/attraction_model.dart create mode 100644 lib/booking/models/booking_model.dart create mode 100644 lib/booking/repositories/booking_repository.dart create mode 100644 lib/booking/viewmodels/booking_viewmodel.dart create mode 100644 lib/booking/views/booking_bottom_sheet.dart create mode 100644 lib/booking/views/booking_page.dart create mode 100644 lib/booking/views/selected_time_slot_page.dart create mode 100644 lib/support/blocs/help_support_bloc.dart create mode 100644 lib/support/blocs/ticket_bloc.dart create mode 100644 lib/support/view/help_support_page.dart create mode 100644 lib/support/view/support_form_page.dart create mode 100644 lib/support/viewmodel/support_model.dart diff --git a/assets/support/icon/material-symbols_policy-rounded.png b/assets/support/icon/material-symbols_policy-rounded.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb90181ace4ac9cd1942330d52dc1830673728a GIT binary patch literal 574 zcmV-E0>S->P)2c!>!w&t>6uWc;D| zvyV&iWs@&CfWIynqV#N%`X0e0AYM$380>q*!-lchRsb(1IjbFwJaW$AArS7Y$uI3J7pOBkDx%-kBfRjB&S>e3+A#ydvaT9(h z3~=V&MexfK3Xn}YXuZ0tbDaJtL*2-Ey;0U*(~ z)cH8Qy3C0>Qln86fjMY*dqU$@B&676Uz#F~b_zsf=CqB8prdym08eH1K7dj2pnWml z)?J-OA#FZeF914GRp+#2GlI(R5~T^h>1s|BqYYvpJi zUspPkO~kp* zp0R*QV2KdI1Occ4%{X2*?3N(w6udOmr!&k7St6Rget~IW04lZAb_fGNhz)Q7T)0H! zMZcLF$ldP#$=>A;z+VS#i%-g}Z6t6AC>jJ*=P8*k4G=IwqQ27!dP@n6&)j{gV88)2 zuoDRawMaLr+WP!A+hb^YU`=$?vA)xObrl%yAw{#RxTZ_Rp1W<@(QRX_~ zgC0e3h|n+u_?$~yl5S1a53s03sluKiz`I?yf^vi63CayjPatDUbvjMf-4+P& zHW|i$x-$%6-)6sk`+d6r{Og8whd-PRpA~QfXxao6vnyI0O%X6dqH*J7boN&O^AGlb z0vvHb2=$g30#^lOH0N#7`vL);?~q(9DiG3N$Gt>5^lmVB97yD}U?MstsS5F(o(zXC z24CMp6~N6$Yti7yOAIh0q|+w=>2H4CD`1F-9Z+tV=XCh$b2?uB-e?gg*RGibNc%5c z>KZQ1#L+rQtzjQO&bEUC=1s)m0Z8>ws$^xxz(eBOq-R~`UC@lA^rl6eLu>s4{(6Cs|@BJCX$l;p1_ zS%!mq{T+PbW_R50*U}P>L6^=J@e#*gfImAK3*wCjAokPQgFu%aC&lp{7P4o;=Y=`S zk@}j7cra}vDL1RP?5>aPux^>jH@B#-okKx`=BLzczM~jVspCOX*Kd`{re*o|{h}(2 zRbufGB!arc+&*NLHC?$ucLLZ1f(|E&xJ8R;rwrt8)4-%baJD4=|Fk~}p71tn>v-n? O0000 bookings; + final bool isLoading; + final List 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? bookings, + bool? isLoading, + List? 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 { + final BookingViewModel viewModel; + + BookingBloc({required this.viewModel}) : super(BookingState.initial()) { + on(_onLoadBookings); + on(_onSelectDate); + on(_onToggleSlotExpand); + } + + Future _onLoadBookings( + LoadBookings event, Emitter 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 emit) { + emit(state.copyWith(selectedDate: event.date)); + } + + void _onToggleSlotExpand(ToggleSlotExpand event, Emitter emit) { + final expanded = List.from(state.expandedAttractions); + if (expanded.contains(event.attractionIndex)) { + expanded.remove(event.attractionIndex); + } else { + expanded.add(event.attractionIndex); + } + emit(state.copyWith(expandedAttractions: expanded)); + } +} diff --git a/lib/booking/models/attraction_model.dart b/lib/booking/models/attraction_model.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/booking/models/booking_model.dart b/lib/booking/models/booking_model.dart new file mode 100644 index 0000000..fbc9a1c --- /dev/null +++ b/lib/booking/models/booking_model.dart @@ -0,0 +1,26 @@ +class BookingDay { + final DateTime date; + final List attractions; + + BookingDay({required this.date, required this.attractions}); +} + +class Attraction { + final String name; + final String colorHex; + final List 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}); +} diff --git a/lib/booking/repositories/booking_repository.dart b/lib/booking/repositories/booking_repository.dart new file mode 100644 index 0000000..11dbc70 --- /dev/null +++ b/lib/booking/repositories/booking_repository.dart @@ -0,0 +1,75 @@ +import '../models/booking_model.dart'; + +class BookingRepository { + Future> 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), + ], + ), + ], + ), + ]; + } +} diff --git a/lib/booking/viewmodels/booking_viewmodel.dart b/lib/booking/viewmodels/booking_viewmodel.dart new file mode 100644 index 0000000..f288d6f --- /dev/null +++ b/lib/booking/viewmodels/booking_viewmodel.dart @@ -0,0 +1,12 @@ +import '../repositories/booking_repository.dart'; +import '../models/booking_model.dart'; + +class BookingViewModel { + final BookingRepository repository; + + BookingViewModel({required this.repository}); + + Future> getBookings(DateTime month) async { + return await repository.fetchBookings(month); + } +} diff --git a/lib/booking/views/booking_bottom_sheet.dart b/lib/booking/views/booking_bottom_sheet.dart new file mode 100644 index 0000000..46425b1 --- /dev/null +++ b/lib/booking/views/booking_bottom_sheet.dart @@ -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(); + 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( + 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)), + ), + ], + ), + ); + } +} diff --git a/lib/booking/views/booking_page.dart b/lib/booking/views/booking_page.dart new file mode 100644 index 0000000..01afe05 --- /dev/null +++ b/lib/booking/views/booking_page.dart @@ -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( + builder: (context, state) { + final bloc = context.read(); + 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, + ), + ), + ), + ), + ); + } +} diff --git a/lib/booking/views/selected_time_slot_page.dart b/lib/booking/views/selected_time_slot_page.dart new file mode 100644 index 0000000..b909e92 --- /dev/null +++ b/lib/booking/views/selected_time_slot_page.dart @@ -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), + ], + ), + ); + } +} diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index ef2b430..7e051d9 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -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 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: (_) => diff --git a/lib/main.dart b/lib/main.dart index 79145c7..75c3e76 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, ); } diff --git a/lib/support/blocs/help_support_bloc.dart b/lib/support/blocs/help_support_bloc.dart new file mode 100644 index 0000000..f9718e6 --- /dev/null +++ b/lib/support/blocs/help_support_bloc.dart @@ -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 faqs; + final bool isLoading; + + HelpSupportState({required this.faqs, required this.isLoading}); + + factory HelpSupportState.initial() => HelpSupportState(faqs: [], isLoading: false); + + HelpSupportState copyWith({ + List? faqs, + bool? isLoading, + }) { + return HelpSupportState( + faqs: faqs ?? this.faqs, + isLoading: isLoading ?? this.isLoading, + ); + } +} + +class HelpSupportBloc extends Bloc { + HelpSupportBloc() : super(HelpSupportState.initial()) { + on(_onLoadFAQ); + } + + Future _onLoadFAQ(LoadFAQ event, Emitter 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)); + } +} \ No newline at end of file diff --git a/lib/support/blocs/ticket_bloc.dart b/lib/support/blocs/ticket_bloc.dart new file mode 100644 index 0000000..6f4b49c --- /dev/null +++ b/lib/support/blocs/ticket_bloc.dart @@ -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 { + TicketBloc() : super(TicketState.initial()) { + on((event, emit) => emit(state.copyWith(subject: event.subject))); + on((event, emit) => emit(state.copyWith(description: event.description))); + on(_onUploadFile); + on(_onSubmitTicket); + } + + Future _onUploadFile(UploadFile event, Emitter emit) async { + final result = await FilePicker.platform.pickFiles(); + if (result != null && result.files.isNotEmpty) { + emit(state.copyWith(selectedFile: result.files.first)); + } + } + + Future _onSubmitTicket(SubmitTicket event, Emitter emit) async { + emit(state.copyWith(isLoading: true)); + await Future.delayed(const Duration(seconds: 2)); // mock API call + emit(state.copyWith(isLoading: false)); + } +} \ No newline at end of file diff --git a/lib/support/view/help_support_page.dart b/lib/support/view/help_support_page.dart new file mode 100644 index 0000000..079bacc --- /dev/null +++ b/lib/support/view/help_support_page.dart @@ -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( + 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)), + ), + ), + ) + ], + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/support/view/support_form_page.dart b/lib/support/view/support_form_page.dart new file mode 100644 index 0000000..9434300 --- /dev/null +++ b/lib/support/view/support_form_page.dart @@ -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(); + return Scaffold( + + backgroundColor: Colors.white, + bottomNavigationBar: BlocBuilder( + 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( + 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), + + ], + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/support/viewmodel/support_model.dart b/lib/support/viewmodel/support_model.dart new file mode 100644 index 0000000..05cfe77 --- /dev/null +++ b/lib/support/viewmodel/support_model.dart @@ -0,0 +1,7 @@ +class SupportItem { + final String title; + final String icon; + final String tag; + + SupportItem(this.title, this.icon, this.tag); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index aa2fd7c..e95b2d1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -65,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" fake_async: dependency: transitive description: @@ -81,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f + url: "https://pub.dev" + source: hosted + version: "10.3.3" flutter: dependency: "direct main" description: flutter @@ -110,11 +142,24 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1+1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + url: "https://pub.dev" + source: hosted + version: "2.0.32" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" google_fonts: dependency: "direct main" description: @@ -139,6 +184,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: @@ -259,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" platform: dependency: transitive description: @@ -283,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://pub.dev" + source: hosted + version: "0.2.1" sky_engine: dependency: transitive description: flutter @@ -320,6 +389,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: @@ -368,6 +445,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -376,6 +461,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 079c0b9..4f29a90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,9 @@ dependencies: google_fonts: ^6.3.2 flutter_bloc: ^9.1.1 flutter_otp_text_field: ^1.5.1+1 + intl: ^0.20.2 + table_calendar: ^3.2.0 + file_picker: ^10.3.3 dev_dependencies: flutter_test: @@ -64,6 +67,7 @@ flutter: assets: - assets/onboarding/ - assets/login/ + - assets/support/ # An image asset can refer to one or more resolution-specific "variants", see