Merge remote-tracking branch 'origin/dinesh' into vinayak
BIN
assets/icons/arrow_angle_up.png
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
assets/images/chicago.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/images/claim_offers_bg.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
assets/images/clock.png
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
assets/images/get_your_pass_bg.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/images/koh_rong.png
Normal file
|
After Width: | Height: | Size: 471 KiB |
BIN
assets/images/lady.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
BIN
assets/images/london_bg.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/images/magic_itenary_bg.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
assets/images/paris_bg.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/images/tokyo_bg.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
@@ -5,8 +5,8 @@ import 'package:citycards_customer/edit_profile/edit_profile_view.dart';
|
||||
import 'package:citycards_customer/esim_offer/esim_offer_view.dart';
|
||||
import 'package:citycards_customer/faq/faq_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_start_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart';
|
||||
import 'package:citycards_customer/privacy/privacy_view.dart';
|
||||
import 'package:citycards_customer/terms_and_condition/terms_and_condition_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import 'explore_cities_card.dart';
|
||||
import '../widgets/explore_cities_card.dart';
|
||||
|
||||
class FirstTimeUserHomePage extends StatefulWidget {
|
||||
const FirstTimeUserHomePage({super.key});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:citycards_customer/home/views/registered_user_home_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -23,6 +24,8 @@ class _HomePageState extends State<HomePage> {
|
||||
switch (state.selectedIndex){
|
||||
case 0:
|
||||
body = const FirstTimeUserHomePage();
|
||||
case 1:
|
||||
body = const RegisteredUserHomePage();
|
||||
break;
|
||||
default:
|
||||
body = const FirstTimeUserHomePage();
|
||||
|
||||
272
lib/home/views/registered_user_home_page.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../widgets/attractions_list.dart';
|
||||
import '../widgets/get_your_pass_card.dart';
|
||||
import '../widgets/gradient_container_bg.dart';
|
||||
import '../widgets/journey_cards_listview.dart';
|
||||
import '../widgets/pass_card_list.dart';
|
||||
|
||||
class RegisteredUserHomePage extends StatefulWidget {
|
||||
const RegisteredUserHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<RegisteredUserHomePage> createState() => _RegisteredUserHomePageState();
|
||||
}
|
||||
|
||||
class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
|
||||
final List<Map<String, String>> attractions = [
|
||||
{
|
||||
'title': 'Long-Tail Boat Charter',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image':
|
||||
'assets/images/clock.png',
|
||||
},
|
||||
{
|
||||
'title': 'Koh Rong Samloemr',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image': 'assets/images/koh_rong.png',
|
||||
},
|
||||
{
|
||||
'title': 'Long-Tail Boat Charter',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image':
|
||||
'assets/images/clock.png',
|
||||
},
|
||||
{
|
||||
'title': 'Koh Rong Samloemr',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image': 'assets/images/koh_rong.png',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/images/chicago.png",
|
||||
height: 300,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: true , isProfilePage: false),
|
||||
const SizedBox(height: 70),
|
||||
Text(
|
||||
"Chicago",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 44,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
|
||||
"Cras posuere, nisl id dictum consequat, elit enim tincidunt magna...",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Category tags
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
_buildTag("Food"),
|
||||
_buildTag("Drinks"),
|
||||
_buildTag("Culture"),
|
||||
_buildTag("Souvenirs"),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: const [
|
||||
TextSpan(
|
||||
text: "Popular ",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Attractions",
|
||||
style:
|
||||
TextStyle(fontSize: 18, color: Colors.black, fontWeight: FontWeight.w500,),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: (){},
|
||||
child: Text("View all",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AttractionsListView(attractions: attractions),
|
||||
],
|
||||
),
|
||||
),
|
||||
InwardCurvedContainer(
|
||||
child: Stack(
|
||||
children: [
|
||||
DreamJourneySection()
|
||||
]
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_buildFeatureCard(
|
||||
image: "assets/images/claim_offers_bg.jpg",
|
||||
title: "Claim offers with your City Cards",
|
||||
subtitle: "Lorem ipsum dolor sit amet...",
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
ChooseYourPassSection(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
// ===== GET YOUR PASS SECTION =====
|
||||
GetYourPassCard(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration:
|
||||
BoxDecoration(color: Color(0xffF95F62), borderRadius: BorderRadius.circular(20)),
|
||||
child: Text(label,
|
||||
style:TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.w500, fontSize: 12)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureCard({
|
||||
required String image,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) {
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image.asset(
|
||||
image,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Left side text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Right side arrow button
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffFDCDCE),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Image.asset(
|
||||
"assets/icons/arrow_angle_up.png",
|
||||
scale: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
106
lib/home/widgets/attractions_list.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AttractionsListView extends StatefulWidget {
|
||||
final List<Map<String, String>> attractions;
|
||||
|
||||
const AttractionsListView({super.key, required this.attractions});
|
||||
|
||||
@override
|
||||
State<AttractionsListView> createState() => _AttractionsListViewState();
|
||||
}
|
||||
|
||||
class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
double _scrollProgress = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_updateScrollProgress);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateScrollProgress() {
|
||||
if (!_scrollController.hasClients ||
|
||||
_scrollController.position.maxScrollExtent == 0) return;
|
||||
setState(() {
|
||||
_scrollProgress = (_scrollController.offset /
|
||||
_scrollController.position.maxScrollExtent)
|
||||
.clamp(0.0, 1.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 240,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
itemCount: widget.attractions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.attractions[index];
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF95F62).withOpacity(0.24),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
height: 232,
|
||||
width: 161,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
image: DecorationImage(
|
||||
image: AssetImage(item['image']!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.bottomLeft,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
item['title']!,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
value: _scrollProgress,
|
||||
minHeight: 6,
|
||||
backgroundColor: const Color(0xffFEE7E7),
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
202
lib/home/widgets/get_your_pass_card.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class GetYourPassCard extends StatelessWidget {
|
||||
const GetYourPassCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF1F1),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// ===== Left Section =====
|
||||
Row(
|
||||
children: [
|
||||
// Left texts and overlapping images
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Get your Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
// Stacked circular attraction images
|
||||
AttractionsAvatarStack(imagePath: 'assets/images/get_your_pass_bg.jpg',),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Attractions",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ===== Right Section =====
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"From",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "\$20",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " /Adult",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Circular Arrow Button
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffF95F62),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AttractionsAvatarStack extends StatelessWidget {
|
||||
const AttractionsAvatarStack({
|
||||
super.key,
|
||||
required this.imagePath, // from your assets/figma
|
||||
this.size = 26, // circle diameter
|
||||
this.count = 4, // total circles including the last “16+”
|
||||
this.overlap = 8, // how much they overlap
|
||||
this.moreText = '16+',
|
||||
});
|
||||
|
||||
final String imagePath;
|
||||
final double size;
|
||||
final int count;
|
||||
final double overlap;
|
||||
final String moreText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final step = size - overlap; // horizontal step between circles
|
||||
return SizedBox(
|
||||
width: size + (count - 1) * step,
|
||||
height: size,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: List.generate(count, (i) {
|
||||
final left = i * step;
|
||||
final isLast = i == count - 1;
|
||||
|
||||
return Positioned(
|
||||
left: left,
|
||||
child: _AvatarCircle(
|
||||
size: size,
|
||||
imagePath: imagePath,
|
||||
showOverlayText: isLast,
|
||||
overlayText: moreText,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarCircle extends StatelessWidget {
|
||||
const _AvatarCircle({
|
||||
required this.size,
|
||||
required this.imagePath,
|
||||
this.showOverlayText = false,
|
||||
this.overlayText = '16+',
|
||||
});
|
||||
|
||||
final double size;
|
||||
final String imagePath;
|
||||
final bool showOverlayText;
|
||||
final String overlayText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: const EdgeInsets.all(1.5), // white ring thickness
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white, // ring color
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset(imagePath, fit: BoxFit.cover),
|
||||
if (showOverlayText)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.35),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
overlayText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
lib/home/widgets/gradient_container_bg.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InwardCurvedContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
const InwardCurvedContainer({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipPath(
|
||||
clipper: InwardAndBottomConvexClipper(),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 700,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFFFF5F5),
|
||||
Color(0xFFFDCDCE),
|
||||
Color(0xFFFFF5F5),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InwardAndBottomConvexClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
Path path = Path();
|
||||
|
||||
// 👇 Start at top-left corner
|
||||
path.moveTo(0, 0);
|
||||
|
||||
// ===== Top inward (concave) curve =====
|
||||
path.quadraticBezierTo(
|
||||
size.width / 2, 60, // Control point (lower Y = deeper dip)
|
||||
size.width, 0, // End of top curve
|
||||
);
|
||||
|
||||
// Right edge down
|
||||
path.lineTo(size.width, size.height - 80);
|
||||
|
||||
// ===== Bottom outward (convex) curve =====
|
||||
path.quadraticBezierTo(
|
||||
size.width / 2, size.height + 20, // Control point (higher Y = bulge outward)
|
||||
0, size.height - 80, // End of bottom curve
|
||||
);
|
||||
|
||||
// Close back to start
|
||||
path.close();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
|
||||
}
|
||||
303
lib/home/widgets/journey_cards_listview.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class DreamJourneySection extends StatefulWidget {
|
||||
const DreamJourneySection({super.key});
|
||||
|
||||
@override
|
||||
State<DreamJourneySection> createState() => _DreamJourneySectionState();
|
||||
}
|
||||
|
||||
class _DreamJourneySectionState extends State<DreamJourneySection> {
|
||||
late PageController _pageController;
|
||||
double _currentPage = 1.0;
|
||||
|
||||
final List<Map<String, dynamic>> baseJourneys = [
|
||||
{
|
||||
"city": "Tokyo",
|
||||
"country": "Japan",
|
||||
"days": "4 days",
|
||||
"tags": ["Modern Culture", "Temple"],
|
||||
"image": "assets/images/tokyo.jpg",
|
||||
"itinerary": [
|
||||
{"title": "Senso-ji Temple", "time": "8:00 AM"},
|
||||
{"title": "Tokyo Skytree", "time": "8:00 AM"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"days": "3 days",
|
||||
"tags": ["Art", "Romantic"],
|
||||
"image": "assets/images/paris.jpg",
|
||||
"itinerary": [
|
||||
{"title": "Eiffel Tower", "time": "9:00 AM"},
|
||||
{"title": "Louvre Museum", "time": "11:00 AM"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"city": "Bangkok",
|
||||
"country": "Thailand",
|
||||
"days": "5 days",
|
||||
"tags": ["Culture", "Food"],
|
||||
"image": "assets/images/bangkok.jpg",
|
||||
"itinerary": [
|
||||
{"title": "Wat Arun", "time": "9:00 AM"},
|
||||
{"title": "Floating Market", "time": "1:00 PM"},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
late List<Map<String, dynamic>> journeys;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Duplicate list for infinite scroll illusion
|
||||
journeys = List.generate(9, (i) => baseJourneys[i % baseJourneys.length]);
|
||||
|
||||
_pageController = PageController(initialPage: 1, viewportFraction: 0.75);
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page ?? 1.0;
|
||||
});
|
||||
|
||||
// Infinite loop logic
|
||||
if (_pageController.page == journeys.length - 2) {
|
||||
Future.microtask(() {
|
||||
_pageController.jumpToPage(2);
|
||||
});
|
||||
} else if (_pageController.page == 1) {
|
||||
Future.microtask(() {
|
||||
_pageController.jumpToPage(journeys.length - 3);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- build ---
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 60),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// === Title ===
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Plan Your ",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Dream Journey",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "\nin Just 3 Seconds",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// === 3D Tilted Carousel ===
|
||||
SizedBox(
|
||||
height: 440,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: journeys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final double distance = (_currentPage - index);
|
||||
final double scale = (1 - (distance.abs() * 0.15)).clamp(0.8, 1.0);
|
||||
|
||||
// 👇 3D tilt transform (top tilts out, bottom tilts in)
|
||||
final Matrix4 transform = Matrix4.identity()
|
||||
..setEntry(3, 2, 0.0015) // perspective
|
||||
..rotateX(distance * -0.4); // tilt angle
|
||||
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: transform,
|
||||
child: Opacity(
|
||||
opacity: (1 - distance.abs() * 0.3).clamp(0.6, 1),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: _buildJourneyCard(journeys[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Card Widget ---
|
||||
Widget _buildJourneyCard(Map<String, dynamic> item) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Image + badge
|
||||
// Stack(
|
||||
// children: [
|
||||
// ClipRRect(
|
||||
// borderRadius:
|
||||
// const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
// child: Image.asset(
|
||||
// item['image'],
|
||||
// height: 160,
|
||||
// width: double.infinity,
|
||||
// fit: BoxFit.cover,
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// right: 10,
|
||||
// top: 10,
|
||||
// child: Container(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xffF95F62),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// child: Text(
|
||||
// item['days'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// color: Colors.white,
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(item['city'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
// Text(item['country'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 12, color: Colors.grey[600])),
|
||||
// const SizedBox(height: 8),
|
||||
// Wrap(
|
||||
// spacing: 8,
|
||||
// children: List.generate(
|
||||
// item['tags'].length,
|
||||
// (i) => Container(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 10, vertical: 4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xffFEE7E7),
|
||||
// borderRadius: BorderRadius.circular(20),
|
||||
// ),
|
||||
// child: Text(
|
||||
// item['tags'][i],
|
||||
// style: GoogleFonts.poppins(
|
||||
// color: const Color(0xffF95F62),
|
||||
// fontSize: 11,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// const Divider(),
|
||||
// const SizedBox(height: 8),
|
||||
// Text("Day 1",
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontWeight: FontWeight.w600, fontSize: 13)),
|
||||
// const SizedBox(height: 6),
|
||||
// ...List.generate(
|
||||
// item['itinerary'].length,
|
||||
// (i) {
|
||||
// final activity = item['itinerary'][i];
|
||||
// return Container(
|
||||
// margin: const EdgeInsets.only(bottom: 8),
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xffFEE7E7),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// CircleAvatar(
|
||||
// radius: 12,
|
||||
// backgroundColor: const Color(0xffF95F62),
|
||||
// child: Text(
|
||||
// "${i + 1}",
|
||||
// style: const TextStyle(
|
||||
// color: Colors.white, fontSize: 12),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(activity['title'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w500)),
|
||||
// Text(activity['time'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 11,
|
||||
// color: Colors.grey[700])),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// const Divider(),
|
||||
// const SizedBox(height: 4),
|
||||
// Text("Day 2",
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontWeight: FontWeight.w600, fontSize: 13)),
|
||||
// ],
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
189
lib/home/widgets/pass_card_list.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class ChooseYourPassSection extends StatefulWidget {
|
||||
const ChooseYourPassSection({super.key});
|
||||
|
||||
@override
|
||||
State<ChooseYourPassSection> createState() => _ChooseYourPassSectionState();
|
||||
}
|
||||
|
||||
class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
|
||||
final PageController _pageController = PageController(
|
||||
viewportFraction: 0.92,
|
||||
);
|
||||
|
||||
int _currentPage = 0;
|
||||
|
||||
final List<Map<String, dynamic>> passes = [
|
||||
{
|
||||
"title": "Chicago-\nFLEXI CARD",
|
||||
"price": "\$50",
|
||||
"color": const Color(0xffF95FAF),
|
||||
"bgColor": const Color(0xFFFDE7F1),
|
||||
},
|
||||
{
|
||||
"title": "Chicago-\nUnlimited CARD",
|
||||
"price": "\$120",
|
||||
"color": const Color(0xffF95F62),
|
||||
"bgColor": const Color(0xFFFFE8E8),
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController.addListener(() {
|
||||
final page = _pageController.page ?? 0;
|
||||
setState(() => _currentPage = page.round());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ===== TITLE =====
|
||||
Text(
|
||||
"Choose your Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Dive into an extensive selection of thrilling destinations, "
|
||||
"thoughtfully categorized to help you find the perfect getaway.",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ===== STATIC PAGEVIEW (no animation) =====
|
||||
SizedBox(
|
||||
height: 430,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: passes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = passes[index];
|
||||
return _buildPassCard(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(passes.length, (index) {
|
||||
bool isActive = index == _currentPage;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 40 : 20,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: isActive
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffFEE7E7),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ===== CARD BUILDER =====
|
||||
Widget _buildPassCard(Map<String, dynamic> item) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: item['bgColor'],
|
||||
border: Border.all(color: item['color'].withOpacity(0.6)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['title'],
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: item['color'],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"From ${item['price']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
color: item['color'],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Dive into an extensive selection of thrilling destinations, "
|
||||
"thoughtfully categorized to help you find the perfect getaway.",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[800],
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n"
|
||||
"• Pellentesque vel nisl posuere, ullamcorper nibh.\n"
|
||||
"• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: item['color'],
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Get a Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/art_gallery_selection_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/city_selection_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/date_selection_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/dietary_selection_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/energy_selection_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/historical_site_rating_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/itinerary_completion_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/kids_selection_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/scenic_viewpoints_rating_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/shopping_rating_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/itinerary_creation_steps/wildlife_rating_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import 'itinerary_creation_steps/art_gallery_selection_view.dart';
|
||||
import 'itinerary_creation_steps/city_selection_view.dart';
|
||||
import 'itinerary_creation_steps/date_selection_view.dart';
|
||||
import 'itinerary_creation_steps/dietary_selection_view.dart';
|
||||
import 'itinerary_creation_steps/energy_selection_view.dart';
|
||||
import 'itinerary_creation_steps/historical_site_rating_view.dart';
|
||||
import 'itinerary_creation_steps/itinerary_completion_view.dart';
|
||||
import 'itinerary_creation_steps/kids_selection_view.dart';
|
||||
import 'itinerary_creation_steps/scenic_viewpoints_rating_view.dart';
|
||||
import 'itinerary_creation_steps/shopping_rating_view.dart';
|
||||
import 'itinerary_creation_steps/wildlife_rating_view.dart';
|
||||
|
||||
class ItineraryCreationPage extends StatefulWidget {
|
||||
const ItineraryCreationPage({super.key});
|
||||
|
||||