Completed home page and added routes also added bloc's package in pubspec.yaml

This commit is contained in:
2025-10-14 15:47:16 +05:30
parent a9447fc869
commit 4264ddd623
15 changed files with 538 additions and 255 deletions

BIN
assets/icons/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

30
lib/core/app_router.dart Normal file
View File

@@ -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')),
),
);
}
}
}

View File

@@ -0,0 +1,3 @@
class RouteConstants {
static const String home = '/home';
}

View File

@@ -1,250 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
final List<Map<String, String>> 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<String> 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"),
],
),
);
}
}

View File

@@ -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
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -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<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final ScrollController _scrollController = ScrollController();
double _scrollProgress = 0.0;
final List<Map<String, String>> 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<Map<String, String>> 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"),
],
),
);
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);