diff --git a/assets/icons/arrow.png b/assets/icons/arrow.png new file mode 100644 index 0000000..f2b671b Binary files /dev/null and b/assets/icons/arrow.png differ diff --git a/assets/images/city_germany.jpg b/assets/images/city_germany.jpg new file mode 100644 index 0000000..67ef57e Binary files /dev/null and b/assets/images/city_germany.jpg differ diff --git a/assets/images/city_maldives.jpg b/assets/images/city_maldives.jpg new file mode 100644 index 0000000..e5d968b Binary files /dev/null and b/assets/images/city_maldives.jpg differ diff --git a/assets/images/city_switz.jpg b/assets/images/city_switz.jpg new file mode 100644 index 0000000..f0c9a59 Binary files /dev/null and b/assets/images/city_switz.jpg differ diff --git a/assets/images/city_sydney.png b/assets/images/city_sydney.png new file mode 100644 index 0000000..7348401 Binary files /dev/null and b/assets/images/city_sydney.png differ diff --git a/assets/images/city_turkey.jpg b/assets/images/city_turkey.jpg new file mode 100644 index 0000000..691d6db Binary files /dev/null and b/assets/images/city_turkey.jpg differ diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart new file mode 100644 index 0000000..732884a --- /dev/null +++ b/lib/core/app_router.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../home/views/home_page_view.dart'; +import 'route_constants.dart'; + +// Uncomment these when blocs are ready +// import '../login/blocs/login_bloc.dart'; +// import '../home/blocs/home_bloc.dart'; + +class AppRouter { + Route onGenerateRoute(RouteSettings settings) { + switch (settings.name) { + case '/': + case RouteConstants.home: + return MaterialPageRoute( + builder: (_) { + // return BlocProvider(create: (_) => HomeBloc(), child: const HomePage()); + return const HomePage(); + }, + ); + + default: + return MaterialPageRoute( + builder: (_) => const Scaffold( + body: Center(child: Text('404 - Page Not Found')), + ), + ); + } + } +} diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart new file mode 100644 index 0000000..d07cddb --- /dev/null +++ b/lib/core/route_constants.dart @@ -0,0 +1,3 @@ +class RouteConstants { + static const String home = '/home'; +} diff --git a/lib/home/home_page_view.dart b/lib/home/home_page_view.dart deleted file mode 100644 index c2139c7..0000000 --- a/lib/home/home_page_view.dart +++ /dev/null @@ -1,250 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter/material.dart'; - -class HomePage extends StatelessWidget { - final List> featuredCities = [ - { - "name": "Melbourne", - "description": "Australia's cultural capital famous for vibrant...", - "individualTicket": "\$350+", - "cityCard": "\$199", - "image": - "https://images.unsplash.com/photo-1536053299937-9b4d6a4a07d1?fit=crop&w=800&q=80" - }, - { - "name": "Sydney", - "description": "Australia's cultural capital famous for vibrant...", - "individualTicket": "\$400+", - "cityCard": "\$249", - "image": - "https://images.unsplash.com/photo-1505575967452-2e9b0a1c0f59?fit=crop&w=800&q=80" - }, - ]; - - final List upcomingCities = [ - "Turkey", - "Germany", - "Switzerland", - "Maldives", - "Turkey", - ]; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: SingleChildScrollView( - child: Stack( - children: [ - Image.asset( - "assets/images/home_bg.png", - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Image.asset( - "assets/logo/logo_city_cards.png", - height: 50, - ), - Row( - children: [ - Container( - padding: EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Image.asset( - "assets/icons/shopping_cart.png", - height: 20, - ), - ), - SizedBox(width: 8), - CircleAvatar( - backgroundColor: Color(0xffFFDFDF), - backgroundImage: AssetImage("assets/images/profile_img.png"), - ) - ], - ), - ], - ), - SizedBox(height: 140), - Text( - "CityCards.\nSee More,\nSpend Less.", - style: TextStyle( - fontSize: 44, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(height: 8), - Text( - "Instant QR access to 40+ attractions, exclusive perks, and savings up to 30%", - style: TextStyle(color: Colors.white), - ), - SizedBox(height: 16), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), - ), - onPressed: () {}, - child: Text("Get Your CityCard"), - ), - SizedBox(height: 32), - - // Featured Cities - Text( - "Explore Cities", - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.black), - ), - SizedBox(height: 8), - Text( - "Explore your dream destination and experience various attractions.", - style: TextStyle(color: Colors.grey[600]), - ), - SizedBox(height: 16), - SizedBox( - height: 220, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: featuredCities.length, - separatorBuilder: (_, __) => SizedBox(width: 16), - itemBuilder: (context, index) { - final city = featuredCities[index]; - return Container( - width: 180, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - image: DecorationImage( - image: NetworkImage(city['image']!), - fit: BoxFit.cover), - ), - child: Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withOpacity(0.7), - Colors.transparent - ], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric( - vertical: 2, horizontal: 6), - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - "Save \$151+", - style: TextStyle( - color: Colors.white, fontSize: 12), - ), - ), - SizedBox(height: 8), - Text( - city['name']!, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold), - ), - SizedBox(height: 4), - Text( - city['description']!, - style: TextStyle( - color: Colors.white70, fontSize: 12), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 4), - Text( - "Individual tickets: ${city['individualTicket']}\nCity Card: ${city['cityCard']}", - style: TextStyle( - color: Colors.white, fontSize: 12), - ) - ], - ), - ), - ); - }, - ), - ), - - SizedBox(height: 32), - // Upcoming Cities - Text( - "Upcoming Cities", - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.black), - ), - SizedBox(height: 8), - Text( - "Explore your dream destination and experience various attractions.", - style: TextStyle(color: Colors.grey[600]), - ), - SizedBox(height: 16), - SizedBox( - height: 80, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: upcomingCities.length, - separatorBuilder: (_, __) => SizedBox(width: 16), - itemBuilder: (context, index) { - return Column( - children: [ - CircleAvatar( - radius: 28, - backgroundImage: NetworkImage( - "https://source.unsplash.com/80x80/?${upcomingCities[index]}"), - ), - SizedBox(height: 4), - Text(upcomingCities[index], - style: TextStyle(fontSize: 12)), - ], - ); - }, - ), - ), - ], - ), - ), - ], - ), - ), - ), - bottomNavigationBar: BottomNavigationBar( - currentIndex: 0, - selectedItemColor: Colors.red, - unselectedItemColor: Colors.grey, - items: [ - BottomNavigationBarItem(icon: Icon(Icons.explore), label: "Explore"), - BottomNavigationBarItem( - icon: Icon(Icons.auto_fix_high), label: "Magic Itinerary"), - BottomNavigationBarItem(icon: Icon(Icons.card_giftcard), label: "My Passes"), - BottomNavigationBarItem(icon: Icon(Icons.post_add), label: "Postcard"), - ], - ), - ); - } -} diff --git a/lib/home/views/explore_cities_card.dart b/lib/home/views/explore_cities_card.dart new file mode 100644 index 0000000..e80d0ee --- /dev/null +++ b/lib/home/views/explore_cities_card.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ExploreCitiesCard extends StatelessWidget { + final String name; + final String description; + final String imageUrl; + final String individualPrice; + final String cityCardPrice; + final String savingsText; + + const ExploreCitiesCard({ + super.key, + required this.name, + required this.description, + required this.imageUrl, + required this.individualPrice, + required this.cityCardPrice, + required this.savingsText, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 220, + margin: const EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + image: DecorationImage( + image: AssetImage(imageUrl), + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.2), + Colors.transparent, + ], + ), + ), + ), + + Positioned( + top: 10, + right: 10, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + decoration: BoxDecoration( + color: const Color(0xffDBFCE7), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + savingsText, + style: GoogleFonts.poppins( + color: const Color(0xFF2C8354), + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ), + ), + + // Bottom text + Positioned( + bottom: 10, + left: 10, + right: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 11, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + + // Prices + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Individual tickets :", + style: TextStyle( + color: Color(0xffFDCDCE), + fontSize: 12 + ), + ), + Text( + individualPrice, + style: TextStyle( + color: Color(0xffFDCDCE), + fontSize: 12, + decoration: TextDecoration.lineThrough, + decorationColor: Color(0xffFDCDCE) + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "City Card :", + style: TextStyle( + color: Color(0xffFDCDCE), + fontSize: 12 + ), + ), + Text( + cityCardPrice, + style: TextStyle( + color: Color(0xffFDCDCE), + fontSize: 12 + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart new file mode 100644 index 0000000..9893294 --- /dev/null +++ b/lib/home/views/home_page_view.dart @@ -0,0 +1,316 @@ +import 'package:citycards_customer/home/views/explore_cities_card.dart'; +import 'package:flutter/material.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final ScrollController _scrollController = ScrollController(); + + double _scrollProgress = 0.0; + + final List> featuredCities = [ + { + "name": "Melbourne", + "description": "Australia's cultural capital famous for vibrant...", + "individualTicket": "\$350+", + "cityCard": "\$199", + "savings": "Save \$151+", + "image": + "assets/images/city_sydney.png" + }, + { + "name": "Sydney", + "description": "Australia's cultural capital famous for vibrant...", + "individualTicket": "\$400+", + "cityCard": "\$249", + "savings": "Save \$151+", + "image": + "assets/images/city_sydney.png" + }, + { + "name": "Sydney", + "description": "Australia's cultural capital famous for vibrant...", + "individualTicket": "\$400+", + "cityCard": "\$249", + "savings": "Save \$151+", + "image": + "assets/images/city_sydney.png" + } + ]; + + final List> upcomingCities = [ + { + "image": "assets/images/city_turkey.jpg", + "name": "Turkey", + }, + { + "image":"assets/images/city_germany.jpg", + "name": "Germany" + }, + { + "image":"assets/images/city_switz.jpg", + "name" : "Switzerland" + }, + { + "image":"assets/images/city_maldives.jpg", + "name": "Maldives" + }, + { + "image" : "assets/images/city_turkey.jpg", + "name": "Turkey" + }, + { + "image":"assets/images/city_germany.jpg", + "name": "Germany" + }, + { + "image":"assets/images/city_switz.jpg", + "name" : "Switzerland" + }, + { + "image":"assets/images/city_maldives.jpg", + "name": "Maldives" + }, + ]; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_updateScrollProgress); + } + + void _updateScrollProgress() { + if (!_scrollController.hasClients || + _scrollController.position.maxScrollExtent == 0) return; + setState(() { + _scrollProgress = (_scrollController.offset / + _scrollController.position.maxScrollExtent) + .clamp(0.0, 1.0); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + child: Stack( + children: [ + Image.asset("assets/images/home_bg.png"), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Image.asset("assets/logo/logo_city_cards.png", + height: 50), + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Image.asset( + "assets/icons/shopping_cart.png", + height: 20, + ), + ), + const SizedBox(width: 8), + const CircleAvatar( + backgroundColor: Color(0xffFFDFDF), + backgroundImage: + AssetImage("assets/images/profile_img.png"), + ), + ], + ), + ], + ), + const SizedBox(height: 140), + const Text( + "CityCards.\nSee More,\nSpend Less.", + style: TextStyle( + fontSize: 44, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + const Text( + "Instant QR access to 40+ attractions,\nexclusive perks, and savings up to 30%", + style: TextStyle(color: Colors.white), + ), + const SizedBox(height: 20), + ElevatedButton( + style: ElevatedButton.styleFrom( + fixedSize: const Size(200, 50), + padding: const EdgeInsets.all(15), + backgroundColor: const Color(0xffF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Get You'r CityCard", + style: TextStyle(color: Colors.white)), + const SizedBox(width: 10), + Image.asset("assets/icons/arrow.png", height: 13), + ], + ), + ), + const SizedBox(height: 80), + Text.rich( + TextSpan( + children: const [ + TextSpan( + text: "Explore ", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), + ), + TextSpan( + text: "Cities", + style: + TextStyle(fontSize: 24, color: Colors.black, fontWeight: FontWeight.w500,), + ), + ], + ), + ), + const SizedBox(height: 8), + const Text( + "Explore your dream destination and experience various attractions.", + style: TextStyle(color: Color(0xff676D75)), + ), + const SizedBox(height: 16), + + // Horizontal cards + SizedBox( + height: 270, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: featuredCities.length, + itemBuilder: (context, index) { + final city = featuredCities[index]; + return ExploreCitiesCard( + name: city['name']!, + description: city['description']!, + imageUrl: city['image']!, + individualPrice: city['individualTicket']!, + cityCardPrice: city['cityCard']!, + savingsText: city['savings']!, + ); + }, + ), + ), + + const SizedBox(height: 10), + const SizedBox(height: 10), + Align( + alignment: Alignment.center, + child: SizedBox( + width: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + value: _scrollProgress, + minHeight: 6, + backgroundColor: Color(0xffFEE7E7), + color: const Color(0xffF95F62), + ), + ), + ), + ), + + + const SizedBox(height: 40), + Text.rich( + TextSpan( + children: const [ + TextSpan( + text: "Upcoming ", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), + ), + TextSpan( + text: "Cities", + style: + TextStyle(fontSize: 24, color: Colors.black, fontWeight: FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 8), + Text( + "Explore your dream destination and experience various attractions.", + style: TextStyle(color: Colors.grey[600]), + ), + const SizedBox(height: 16), + SizedBox( + height: 80, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: upcomingCities.length, + separatorBuilder: (_, __) => const SizedBox(width: 16), + itemBuilder: (context, index) { + return Column( + children: [ + CircleAvatar( + radius: 28, + backgroundImage: AssetImage(upcomingCities[index]["image"] ?? ""), + ), + const SizedBox(height: 4), + Text(upcomingCities[index]["name"] ?? "", + style: const TextStyle(fontSize: 12)), + ], + ); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: 0, + selectedItemColor: Colors.red, + unselectedItemColor: Colors.grey, + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.explore), label: "Explore"), + BottomNavigationBarItem( + icon: Icon(Icons.auto_fix_high), label: "Magic Itinerary"), + BottomNavigationBarItem( + icon: Icon(Icons.card_giftcard), label: "My Passes"), + BottomNavigationBarItem( + icon: Icon(Icons.post_add), label: "Postcard"), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 2ed2fba..37d63ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'home/home_page_view.dart'; +import 'core/app_router.dart'; +import 'core/route_constants.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -11,15 +12,19 @@ void main() { statusBarIconBrightness: Brightness.dark, statusBarBrightness: Brightness.light, )); - runApp(const MyApp()); + runApp(MyApp()); } class MyApp extends StatelessWidget { - const MyApp({super.key}); + MyApp({super.key}); + + final AppRouter _appRouter = AppRouter(); @override Widget build(BuildContext context) { return MaterialApp( + onGenerateRoute: _appRouter.onGenerateRoute, + initialRoute: RouteConstants.home, debugShowCheckedModeBanner: false, title: 'City Cards', theme: ThemeData( @@ -27,7 +32,6 @@ class MyApp extends StatelessWidget { Theme.of(context).textTheme, ) ), - home: HomePage(), ); } } diff --git a/pubspec.lock b/pubspec.lock index e2bc01f..a9c288f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: e18b8e7825e9921d67a6d256dba0b6015ece8a577eb0a411845c46a352994d78 + url: "https://pub.dev" + source: hosted + version: "9.0.1" boolean_selector: dependency: transitive description: @@ -78,6 +86,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.dev" + source: hosted + version: "9.1.1" flutter_lints: dependency: "direct dev" description: @@ -171,6 +187,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -243,6 +267,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 80a6bcf..ad07b1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ environment: # versions available, run `flutter pub outdated`. dependencies: google_fonts: ^6.3.2 + flutter_bloc: ^9.1.1 flutter: sdk: flutter diff --git a/test/widget_test.dart b/test/widget_test.dart index 3103156..80d4876 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:citycards_customer/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);