working on my pass section
This commit is contained in:
BIN
assets/images/empty_buy_ pass.png
Normal file
BIN
assets/images/empty_buy_ pass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/images/qr_image.png
Normal file
BIN
assets/images/qr_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -14,6 +14,11 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
|
||||
emit(AttractionsLoaded(attractions));
|
||||
});
|
||||
|
||||
on<LoadMyPassAttraction>((event, emit) {
|
||||
final attractions = repository.fetchMyPassAttraction();
|
||||
emit(AttractionsLoaded(attractions));
|
||||
});
|
||||
|
||||
on<SearchAttractions>((event, emit) {
|
||||
if (state is AttractionsLoaded) {
|
||||
final currentState = state as AttractionsLoaded;
|
||||
|
||||
@@ -4,6 +4,8 @@ abstract class AttractionsEvent {}
|
||||
|
||||
class LoadAttractions extends AttractionsEvent {}
|
||||
|
||||
class LoadMyPassAttraction extends AttractionsEvent {}
|
||||
|
||||
class SearchAttractions extends AttractionsEvent {
|
||||
final String query;
|
||||
SearchAttractions(this.query);
|
||||
|
||||
@@ -4,6 +4,8 @@ class Attraction {
|
||||
final String price;
|
||||
final String image;
|
||||
final List<String> tags;
|
||||
final bool isBookingRequired;
|
||||
final String description;
|
||||
|
||||
Attraction({
|
||||
required this.title,
|
||||
@@ -11,5 +13,7 @@ class Attraction {
|
||||
required this.price,
|
||||
required this.image,
|
||||
required this.tags,
|
||||
required this.isBookingRequired,
|
||||
required this.description
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_1.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Siem Reap",
|
||||
@@ -16,6 +19,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_2.jpg",
|
||||
tags: ["Unlimited Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
@@ -23,6 +29,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_3.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Koh Rong Samloem",
|
||||
@@ -30,6 +39,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_4.jpg",
|
||||
tags: ["Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
@@ -37,6 +49,64 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_5.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Attraction> fetchMyPassAttraction() {
|
||||
return [
|
||||
Attraction(
|
||||
title: "Koh Rong Samloem",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_1.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Siem Reap",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_2.jpg",
|
||||
tags: ["Unlimited Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_3.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Koh Rong Samloem",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_4.jpg",
|
||||
tags: ["Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_5.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/back_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@@ -9,12 +10,24 @@ import '../widget/attraction_card.dart';
|
||||
import '../widget/filter_chip.dart';
|
||||
|
||||
class AttractionsPage extends StatelessWidget {
|
||||
const AttractionsPage({super.key});
|
||||
final String source;
|
||||
const AttractionsPage({super.key, required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => AttractionsBloc(AttractionsRepository())..add(LoadAttractions()),
|
||||
create: (_) {
|
||||
final bloc = AttractionsBloc(AttractionsRepository());
|
||||
|
||||
// 🔥 Trigger event based on source
|
||||
if (source == "home") {
|
||||
bloc.add(LoadAttractions());
|
||||
} else if (source == "qrPass") {
|
||||
bloc.add(LoadMyPassAttraction());
|
||||
}
|
||||
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<AttractionsBloc, AttractionsState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<AttractionsBloc>();
|
||||
@@ -29,26 +42,7 @@ class AttractionsPage extends StatelessWidget {
|
||||
children: [
|
||||
// App bar
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 22.h),
|
||||
|
||||
// Back row
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Icon(Icons.arrow_back, size: 24.sp),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
"Your Attraction",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backWidget(context, "Your Attraction"),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 🔍 Search field
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../models/attraction_model.dart';
|
||||
|
||||
class AttractionCard extends StatelessWidget {
|
||||
@@ -8,91 +9,133 @@ class AttractionCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: Color(0xffFFF5F5),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
attraction.image,
|
||||
height: 94,
|
||||
width: 94,
|
||||
fit: BoxFit.cover,
|
||||
return InkWell(
|
||||
onTap: (){
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionDetails);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: Color(0xffFFF5F5),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
attraction.image,
|
||||
height: 94,
|
||||
width: 94,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(attraction.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 6),
|
||||
Text(attraction.location,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Color(0xff464646))),
|
||||
const SizedBox(height: 6),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "from ${attraction.price}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: "/person",
|
||||
style:
|
||||
TextStyle(fontSize: 10, color: Colors.black, fontWeight: FontWeight.w400,),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attraction.title,
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: attraction.tags
|
||||
.map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF).withOpacity(0.1)
|
||||
: const Color(0xffF95F62).withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF)
|
||||
: const Color(0xffF95F62),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
attraction.location,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xff464646),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "from ${attraction.price}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: "/person",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
)
|
||||
,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
attraction.isBookingRequired == false
|
||||
? Wrap(
|
||||
spacing: 6,
|
||||
children: attraction.tags
|
||||
.map(
|
||||
(tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF).withOpacity(0.1)
|
||||
: const Color(
|
||||
0xffF95F62,
|
||||
).withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF)
|
||||
: const Color(0xffF95F62),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffC1D2F8),
|
||||
border: Border.all(
|
||||
color: Color(0xff2563EB),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"Booking Required",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ class AppRouter {
|
||||
},
|
||||
);
|
||||
case RouteConstants.attractionsPage:
|
||||
return MaterialPageRoute(builder: (_) => const AttractionsPage());
|
||||
final args = settings.arguments as String;
|
||||
return MaterialPageRoute(builder: (_) => AttractionsPage(source: args));
|
||||
case RouteConstants.profile:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../attraction_details/attraction_details_view.dart';
|
||||
import '../attractions/views/attractions_page_view.dart';
|
||||
import '../my_pass/views/qr_pass_page_view.dart';
|
||||
import '../postcard/blocs/postcard_creation_bloc.dart';
|
||||
import '../postcard/views/postcard_creation_page_view.dart';
|
||||
|
||||
@@ -24,10 +27,15 @@ Widget buildOffstageNavigator(
|
||||
|
||||
// 🔹 Attractions Page
|
||||
case RouteConstants.attractionsPage:
|
||||
final args = settings.arguments as String;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const AttractionsPage(),
|
||||
builder: (_) => AttractionsPage(source: args,),
|
||||
);
|
||||
|
||||
case RouteConstants.attractionDetails:
|
||||
return MaterialPageRoute(builder: (_) {
|
||||
return AttractionDetailsView();
|
||||
});
|
||||
// 🔹 Upload Photo Page (start of postcard creation flow)
|
||||
case RouteConstants.uploadPhotoPage:
|
||||
return MaterialPageRoute(
|
||||
@@ -49,6 +57,18 @@ Widget buildOffstageNavigator(
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
case RouteConstants.qrPage:
|
||||
return MaterialPageRoute(
|
||||
builder: (context) {
|
||||
final previousBloc = BlocProvider.of<MyPassBloc>(context);
|
||||
return BlocProvider.value(
|
||||
value: previousBloc,
|
||||
child: const QrPassView(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const Scaffold(
|
||||
|
||||
@@ -42,4 +42,6 @@ class RouteConstants {
|
||||
/************************** My card page ***************************************/
|
||||
static const String cartPage = '/cartPage';
|
||||
|
||||
|
||||
static const String qrPage = '/qrPage';
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../common_packages/custom_bottom_navbar.dart';
|
||||
import '../../core/inside_bottom_navigator.dart';
|
||||
import '../../itinerary_creation/views/itinerary_creation_start_view.dart';
|
||||
import '../../my_pass/views/my_pass_page_view.dart';
|
||||
import '../../postcard/views/postcard_initial_page_view.dart';
|
||||
import 'first_time_user_home_page.dart';
|
||||
|
||||
@@ -36,6 +37,7 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage(), _navigatorKeys[0]),
|
||||
buildOffstageNavigator(1, currentIndex, const ItineraryCreationStartPage(), _navigatorKeys[1]),
|
||||
buildOffstageNavigator(2, currentIndex, const MyPassesView(), _navigatorKeys[2]),
|
||||
buildOffstageNavigator(3, currentIndex, const PostcardPage(), _navigatorKeys[3]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -127,7 +127,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
),
|
||||
InkWell(
|
||||
onTap: (){
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage);
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage, arguments: "home");
|
||||
},
|
||||
child: Text("View all",
|
||||
style: TextStyle(
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:citycards_customer/cart/blocs/postcard_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'core/app_router.dart';
|
||||
import 'core/route_constants.dart';
|
||||
import 'my_pass/blocs/my_pass_bloc.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white,
|
||||
@@ -14,6 +17,7 @@ void main() {
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
@@ -27,13 +31,20 @@ class MyApp extends StatelessWidget {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (context, child) {
|
||||
return MaterialApp(
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'City Cards',
|
||||
theme: ThemeData(
|
||||
textTheme: GoogleFonts.poppinsTextTheme(
|
||||
Theme.of(context).textTheme,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<MyPassBloc>(
|
||||
create: (_) => MyPassBloc()..add(LoadMyPasses()),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'City Cards',
|
||||
theme: ThemeData(
|
||||
textTheme: GoogleFonts.poppinsTextTheme(
|
||||
Theme.of(context).textTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
55
lib/my_pass/blocs/my_pass_bloc.dart
Normal file
55
lib/my_pass/blocs/my_pass_bloc.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../models/my_pass_model.dart';
|
||||
|
||||
abstract class MyPassEvent {}
|
||||
class LoadMyPasses extends MyPassEvent {}
|
||||
|
||||
abstract class MyPassState {}
|
||||
class MyPassLoading extends MyPassState {}
|
||||
class SelectPass extends MyPassEvent {
|
||||
final MyPassModel selectedPass;
|
||||
SelectPass(this.selectedPass);
|
||||
}
|
||||
class MyPassEmpty extends MyPassState {}
|
||||
class MyPassLoaded extends MyPassState {
|
||||
final List<MyPassModel> passes;
|
||||
final MyPassModel? selectedPass;
|
||||
MyPassLoaded(this.passes, this.selectedPass);
|
||||
}
|
||||
|
||||
class MyPassBloc extends Bloc<MyPassEvent, MyPassState> {
|
||||
MyPassBloc() : super(MyPassLoading()) {
|
||||
|
||||
on<LoadMyPasses>((event, emit) async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final List<MyPassModel> passes = [
|
||||
MyPassModel(
|
||||
imageUrl:
|
||||
"assets/images/city_melbourne.png",
|
||||
title: "Unlimited Card",
|
||||
city: "Melbourne",
|
||||
validity: "20/09/2025",
|
||||
adults: 3,
|
||||
kids: 3,
|
||||
duration: "2 Days",
|
||||
isActive: true,
|
||||
),
|
||||
];
|
||||
|
||||
// If no passes, show empty screen
|
||||
if (passes.isEmpty) {
|
||||
emit(MyPassEmpty());
|
||||
} else {
|
||||
emit(MyPassLoaded(passes, null));
|
||||
}
|
||||
});
|
||||
|
||||
on<SelectPass>((event, emit) {
|
||||
if (state is MyPassLoaded) {
|
||||
final current = state as MyPassLoaded;
|
||||
emit(MyPassLoaded(current.passes, event.selectedPass));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
21
lib/my_pass/models/my_pass_model.dart
Normal file
21
lib/my_pass/models/my_pass_model.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
class MyPassModel {
|
||||
final String imageUrl;
|
||||
final String title;
|
||||
final String city;
|
||||
final String validity;
|
||||
final int adults;
|
||||
final int kids;
|
||||
final String duration;
|
||||
final bool isActive;
|
||||
|
||||
MyPassModel({
|
||||
required this.imageUrl,
|
||||
required this.title,
|
||||
required this.city,
|
||||
required this.validity,
|
||||
required this.adults,
|
||||
required this.kids,
|
||||
required this.duration,
|
||||
required this.isActive,
|
||||
});
|
||||
}
|
||||
168
lib/my_pass/views/my_pass_page_view.dart
Normal file
168
lib/my_pass/views/my_pass_page_view.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../blocs/my_pass_bloc.dart';
|
||||
import '../widgets/pass_widget.dart';
|
||||
|
||||
class MyPassesView extends StatelessWidget {
|
||||
const MyPassesView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPassBloc, MyPassState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is MyPassEmpty) {
|
||||
return _noPassView(context);
|
||||
} else if (state is MyPassLoaded) {
|
||||
return _passListView(state.passes);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _noPassView(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.h),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/no_pass.png', // your woman sitting image
|
||||
height: 180.h,
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Text(
|
||||
"You Don’t have a Pass Yet! 😕",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
"Get a pass and get offers and discounts and\nmore on your trip to your favourite city",
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp, color: Colors.black54),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Navigate to Buy a Pass
|
||||
Navigator.pushNamed(context, '/buyPass');
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFF5A5F),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Buy a Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _passListView(List passes) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 10.h),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 130.w,
|
||||
height: 36.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFEE7E7),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Sort by Date",
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.sort, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Container(
|
||||
height: 36.h,
|
||||
width: 130.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFEE7E7),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"All",
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.keyboard_arrow_down_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
ListView.builder(
|
||||
itemCount: passes.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final pass = passes[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: InkWell(
|
||||
onTap: (){
|
||||
context.read<MyPassBloc>().add(SelectPass(pass));
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamed(RouteConstants.qrPage);
|
||||
},
|
||||
child: PassTicketCard(pass: pass),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
142
lib/my_pass/views/qr_pass_page_view.dart
Normal file
142
lib/my_pass/views/qr_pass_page_view.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/back_widget.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../widgets/action_button_widget.dart';
|
||||
import '../widgets/qr_container_widget.dart';
|
||||
|
||||
class QrPassView extends StatelessWidget {
|
||||
const QrPassView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPassBloc, MyPassState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassLoaded) {
|
||||
final pass = state.selectedPass!;
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 10.h),
|
||||
backWidget(context, "Back"),
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
"Scan this at the site of\nattraction",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// ♻️ Reusable QR Container Component
|
||||
QrContainerWidget(
|
||||
qrImagePath: "assets/images/qr_image.png",
|
||||
cityCardTitle: "Melbourne CityCards",
|
||||
qrCode: "IYFHHVN254ADSD",
|
||||
cardType: pass.title,
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// 🎟 Card details section
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 40,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: pass.title.toLowerCase() == "unlimited card"
|
||||
? const Color(0xffF95F62).withOpacity(0.1)
|
||||
: const Color(0xffF95FAF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
border: Border.all(
|
||||
color: pass.title.toLowerCase() == "unlimited card"
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffF95FAF),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
pass.title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
color: const Color(0xffFF5A5F),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
"Adults-${pass.adults} • Kids-${pass.kids} • ${pass.duration}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff212121),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Valid Till: ${pass.validity}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff212121),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 28.h),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Learn about policies",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// 🔘 Buttons
|
||||
Column(
|
||||
children: [
|
||||
actionButton(
|
||||
label: "View All Attractions",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage, arguments: "qrPass");
|
||||
},
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
actionButton(
|
||||
label: "View All Available Offers",
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/my_pass/widgets/action_button_widget.dart
Normal file
40
lib/my_pass/widgets/action_button_widget.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_screenutil/flutter_screenutil.dart";
|
||||
import "package:google_fonts/google_fonts.dart";
|
||||
|
||||
Widget actionButton({
|
||||
required String label,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
color: const Color(0xffFFF5F5),
|
||||
border: Border.all(color: const Color(0xffF5C2C2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
285
lib/my_pass/widgets/pass_widget.dart
Normal file
285
lib/my_pass/widgets/pass_widget.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class PassTicketCard extends StatelessWidget {
|
||||
final dynamic pass;
|
||||
|
||||
const PassTicketCard({super.key, required this.pass});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Dimensions tuned to your screenshot
|
||||
final double cardWidth = MediaQuery.of(context).size.width - 32.w;
|
||||
final double topSectionHeight = 105.h; // where dotted line sits
|
||||
final double bottomSectionHeight = 50.h;
|
||||
final double cardHeight = topSectionHeight + bottomSectionHeight;
|
||||
|
||||
return SizedBox(
|
||||
width: cardWidth,
|
||||
child: CustomPaint(
|
||||
// paints white background, border, corner radius, side cuts, shadow, and divider dots
|
||||
painter: _TicketBackgroundPainter(
|
||||
cornerRadius: 16.r,
|
||||
notchRadius: 9.r,
|
||||
dividerY: topSectionHeight,
|
||||
borderColor: Colors.white,
|
||||
shadowColor: Colors.black.withOpacity(0.08),
|
||||
),
|
||||
child: ClipPath(
|
||||
// actual clipping so child content never bleeds outside the shape
|
||||
clipper: _TicketClipper(
|
||||
cornerRadius: 16.r,
|
||||
notchRadius: 9.r,
|
||||
dividerY: topSectionHeight,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
|
||||
child: Column(
|
||||
children: [
|
||||
// ---------- TOP SECTION ----------
|
||||
SizedBox(
|
||||
height: topSectionHeight - 12.h, // keep space for the dots line
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// thumbnail
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
child: Image.asset(
|
||||
pass.imageUrl,
|
||||
height: 80.h,
|
||||
width: 80.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
|
||||
// details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (pass.isActive)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8.w, vertical: 3.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff439F6E),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Text(
|
||||
"Active",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
pass.duration, // "2 Days"
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black87,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
pass.title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18.sp,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Adults-${pass.adults} • Kids-${pass.kids}",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black54,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// QR chip
|
||||
CircleAvatar(
|
||||
radius: 20.r,
|
||||
backgroundColor: Color(0xffFEE7E7),
|
||||
child: Image.asset(
|
||||
"assets/images/qr_image.png",
|
||||
scale: 6,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// space exactly where the dotted line is painted by the painter
|
||||
SizedBox(height: 15.h),
|
||||
|
||||
// ---------- BOTTOM SECTION ----------
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Valid Till: ${pass.validity}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.sp,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400
|
||||
),
|
||||
),
|
||||
Text(
|
||||
pass.city, // "Melbourne"
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clips the ticket with rounded corners and 2 side “cuts” centered at dividerY
|
||||
class _TicketClipper extends CustomClipper<Path> {
|
||||
final double cornerRadius;
|
||||
final double notchRadius;
|
||||
final double dividerY;
|
||||
|
||||
_TicketClipper({
|
||||
required this.cornerRadius,
|
||||
required this.notchRadius,
|
||||
required this.dividerY,
|
||||
});
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final rrectPath = Path()
|
||||
..addRRect(RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Radius.circular(cornerRadius),
|
||||
));
|
||||
|
||||
final cuts = Path()
|
||||
..addOval(Rect.fromCircle(center: Offset(0, dividerY), radius: notchRadius))
|
||||
..addOval(Rect.fromCircle(center: Offset(size.width, dividerY), radius: notchRadius));
|
||||
|
||||
// Rounded-rect MINUS the two circles
|
||||
return Path.combine(PathOperation.difference, rrectPath, cuts);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant _TicketClipper old) =>
|
||||
cornerRadius != old.cornerRadius ||
|
||||
notchRadius != old.notchRadius ||
|
||||
dividerY != old.dividerY;
|
||||
}
|
||||
|
||||
|
||||
/// Paints fill, border, shadow and the dotted perforation line
|
||||
class _TicketBackgroundPainter extends CustomPainter {
|
||||
final double cornerRadius;
|
||||
final double notchRadius;
|
||||
final double dividerY;
|
||||
final Color borderColor;
|
||||
final Color shadowColor;
|
||||
|
||||
_TicketBackgroundPainter({
|
||||
required this.cornerRadius,
|
||||
required this.notchRadius,
|
||||
required this.dividerY,
|
||||
required this.borderColor,
|
||||
required this.shadowColor,
|
||||
});
|
||||
|
||||
Path _ticketPath(Size size) {
|
||||
final clipper = _TicketClipper(
|
||||
cornerRadius: cornerRadius,
|
||||
notchRadius: notchRadius,
|
||||
dividerY: dividerY,
|
||||
);
|
||||
return clipper.getClip(size);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final path = _ticketPath(size);
|
||||
|
||||
// Realistic layered shadow
|
||||
canvas.save();
|
||||
canvas.translate(0, 2); // tiny downward offset for depth
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.10)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6);
|
||||
canvas.drawPath(path, shadowPaint);
|
||||
canvas.restore();
|
||||
|
||||
// Subtle ambient shadow (light spread around)
|
||||
final ambientShadowPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.04)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
|
||||
canvas.drawPath(path, ambientShadowPaint);
|
||||
|
||||
// Fill background
|
||||
final fillPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = const Color(0xffFFFBFB);
|
||||
canvas.drawPath(path, fillPaint);
|
||||
|
||||
// Border stroke
|
||||
final strokePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.8
|
||||
..color = const Color(0xffE5E5E5);
|
||||
canvas.drawPath(path, strokePaint);
|
||||
|
||||
// 🔹 Dotted perforation line
|
||||
final dashPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..color = const Color(0xff787878);
|
||||
|
||||
const double dashWidth = 4;
|
||||
const double dashSpace = 4;
|
||||
double startX = 12;
|
||||
final double endX = size.width - 12;
|
||||
|
||||
while (startX < endX) {
|
||||
final double currentEnd = (startX + dashWidth).clamp(0, endX);
|
||||
canvas.drawLine(
|
||||
Offset(startX, dividerY),
|
||||
Offset(currentEnd, dividerY),
|
||||
dashPaint,
|
||||
);
|
||||
startX += dashWidth + dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _TicketBackgroundPainter oldDelegate) {
|
||||
return cornerRadius != oldDelegate.cornerRadius ||
|
||||
notchRadius != oldDelegate.notchRadius ||
|
||||
dividerY != oldDelegate.dividerY ||
|
||||
borderColor != oldDelegate.borderColor ||
|
||||
shadowColor != oldDelegate.shadowColor;
|
||||
}
|
||||
}
|
||||
90
lib/my_pass/widgets/qr_container_widget.dart
Normal file
90
lib/my_pass/widgets/qr_container_widget.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class QrContainerWidget extends StatelessWidget {
|
||||
final String qrImagePath;
|
||||
final String cityCardTitle;
|
||||
final String qrCode;
|
||||
final String cardType;
|
||||
|
||||
const QrContainerWidget({
|
||||
super.key,
|
||||
required this.qrImagePath,
|
||||
required this.cityCardTitle,
|
||||
required this.qrCode,
|
||||
required this.cardType
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 380.h,
|
||||
margin: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF95F62).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
border: Border.all(color: cardType.toLowerCase() == "unlimited card" ? const Color(0xffF95F62) : const Color(0xffF95FAF), width: 2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
cityCardTitle,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18.sp,
|
||||
color: cardType.toLowerCase() == "unlimited card" ? const Color(0xffF95F62) : const Color(0xffF95FAF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
Image.asset(
|
||||
qrImagePath,
|
||||
height: 250.h,
|
||||
width: 250.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
qrCode,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.sp,
|
||||
color: Color(0xff212121)
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: qrCode));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Code copied to clipboard!",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black87,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.copy, size: 18, color: Color(0xff212121)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user