diff --git a/.fvmrc b/.fvmrc
new file mode 100644
index 0000000..b987073
--- /dev/null
+++ b/.fvmrc
@@ -0,0 +1,3 @@
+{
+ "flutter": "3.29.3"
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 79c113f..b6323af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
+
+# FVM Version Cache
+.fvm/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e560150
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "dart.flutterSdkPath": ".fvm/versions/3.29.3"
+}
\ No newline at end of file
diff --git a/assets/icons/SE.png b/assets/icons/SE.png
new file mode 100644
index 0000000..12df24d
Binary files /dev/null and b/assets/icons/SE.png differ
diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg
new file mode 100644
index 0000000..98b921c
--- /dev/null
+++ b/assets/icons/arrow-right.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/arrows.svg b/assets/icons/arrows.svg
new file mode 100644
index 0000000..c5ef0d4
--- /dev/null
+++ b/assets/icons/arrows.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg
new file mode 100644
index 0000000..d93410f
--- /dev/null
+++ b/assets/icons/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/home.svg b/assets/icons/home.svg
new file mode 100644
index 0000000..2920ece
--- /dev/null
+++ b/assets/icons/home.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/homeActive.svg b/assets/icons/homeActive.svg
new file mode 100644
index 0000000..faee82a
--- /dev/null
+++ b/assets/icons/homeActive.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/location.svg b/assets/icons/location.svg
new file mode 100644
index 0000000..53d832d
--- /dev/null
+++ b/assets/icons/location.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/profile-circle.svg b/assets/icons/profile-circle.svg
new file mode 100644
index 0000000..7ec0783
--- /dev/null
+++ b/assets/icons/profile-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/profile-circle_active.svg b/assets/icons/profile-circle_active.svg
new file mode 100644
index 0000000..4a0b012
--- /dev/null
+++ b/assets/icons/profile-circle_active.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/shield.svg b/assets/icons/shield.svg
new file mode 100644
index 0000000..d164189
--- /dev/null
+++ b/assets/icons/shield.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/task-square_active.svg b/assets/icons/task-square_active.svg
new file mode 100644
index 0000000..ec4ccbb
--- /dev/null
+++ b/assets/icons/task-square_active.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/task_square.svg b/assets/icons/task_square.svg
new file mode 100644
index 0000000..94d43b7
--- /dev/null
+++ b/assets/icons/task_square.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/tenderLogo.png b/assets/icons/tenderLogo.png
new file mode 100644
index 0000000..9fd2839
Binary files /dev/null and b/assets/icons/tenderLogo.png differ
diff --git a/assets/icons/tenderLogo.svg b/assets/icons/tenderLogo.svg
new file mode 100644
index 0000000..900b039
--- /dev/null
+++ b/assets/icons/tenderLogo.svg
@@ -0,0 +1,12 @@
+
diff --git a/assets/icons/thumb.svg b/assets/icons/thumb.svg
new file mode 100644
index 0000000..c614def
--- /dev/null
+++ b/assets/icons/thumb.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/pngs/logo.png b/assets/pngs/logo.png
new file mode 100644
index 0000000..9fbd60d
Binary files /dev/null and b/assets/pngs/logo.png differ
diff --git a/assets/svgs/password_icon.svg b/assets/svgs/password_icon.svg
new file mode 100644
index 0000000..c30c824
--- /dev/null
+++ b/assets/svgs/password_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/svgs/password_visibility.svg b/assets/svgs/password_visibility.svg
new file mode 100644
index 0000000..6935a0c
--- /dev/null
+++ b/assets/svgs/password_visibility.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/svgs/user_icon.svg b/assets/svgs/user_icon.svg
new file mode 100644
index 0000000..69c26ad
--- /dev/null
+++ b/assets/svgs/user_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/bottom_navigation.dart b/lib/bottom_navigation.dart
new file mode 100644
index 0000000..1a8dba8
--- /dev/null
+++ b/lib/bottom_navigation.dart
@@ -0,0 +1,97 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import 'size_config.dart';
+
+class BottomNavigation extends StatefulWidget {
+ const BottomNavigation({super.key});
+
+ @override
+ State createState() => _BottomNavigationState();
+}
+
+class _BottomNavigationState extends State {
+ int activeTab = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 72.0.h(),
+ decoration: BoxDecoration(
+ color: Color(0xFFE3E3E3),
+ border: Border.fromBorderSide(BorderSide(color: Color(0xFFE3E3E3))),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _bottomNavigationItem(
+ text: 'Home',
+ isActive: activeTab == 0,
+ onTap: () {
+ setState(() {
+ activeTab = 0;
+ });
+ },
+ iconPath: 'assets/icons/home.svg',
+ activeIconPath: 'assets/icons/homeActive.svg',
+ ),
+ _bottomNavigationItem(
+ text: 'Tenders',
+ isActive: activeTab == 1,
+ onTap: () {
+ setState(() {
+ activeTab = 1;
+ });
+ },
+ iconPath: 'assets/icons/task_square.svg',
+ activeIconPath: 'assets/icons/task-square_active.svg',
+ ),
+ _bottomNavigationItem(
+ text: 'Profile',
+ isActive: activeTab == 2,
+ onTap: () {
+ setState(() {
+ activeTab = 2;
+ });
+ },
+ iconPath: 'assets/icons/profile-circle.svg',
+ activeIconPath: 'assets/icons/profile-circle_active.svg',
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _bottomNavigationItem({
+ required String text,
+ required bool isActive,
+ required VoidCallback onTap,
+ required String iconPath,
+ required String activeIconPath,
+ }) {
+ return InkWell(
+ onTap: onTap,
+ child: SizedBox(
+ width: 64.0.w(),
+ height: 72.0.h(),
+ child: Padding(
+ padding: EdgeInsets.only(top: 12.0.h(), bottom: 14.0.h()),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ SvgPicture.asset(isActive ? activeIconPath : iconPath),
+ Text(
+ text,
+ style: TextStyle(
+ fontSize: 12.0.sp(),
+ fontWeight: FontWeight.w400,
+ color: isActive ? Color(0xFF0164FF) : Color(0xFF777777),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/constants/app_strings.dart b/lib/constants/app_strings.dart
new file mode 100644
index 0000000..b9763af
--- /dev/null
+++ b/lib/constants/app_strings.dart
@@ -0,0 +1,14 @@
+class AppStrings {
+ AppStrings._();
+
+ static const String home = 'Home';
+
+ static const String partnership = 'Partnership';
+ static const String selfApply = 'Self-apply';
+ static const String contracting = 'Contracting';
+ static const String tenderSubmitting = 'Tender submitting';
+ static const String approvedTenders = 'Approved tenders';
+ static const String tenderValue = 'Tender Value';
+ static const String thunderStatus = 'Thunder status';
+ static const String yourTenders = 'Your tenders';
+}
diff --git a/lib/core/constants/assets.dart b/lib/core/constants/assets.dart
new file mode 100644
index 0000000..700a2c0
--- /dev/null
+++ b/lib/core/constants/assets.dart
@@ -0,0 +1,10 @@
+class AssetsManager {
+ AssetsManager._();
+
+ // login page
+ static const logo = 'assets/pngs/logo.png';
+ static const userIcon = 'assets/svgs/user_icon.svg';
+ static const passwordIcon = 'assets/svgs/password_icon.svg';
+ static const passwordVisibility = 'assets/svgs/password_visibility.svg';
+ static const passwordVisibilityOff = 'assets/svgs/password_visibility.svg';
+}
diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart
index c727ee8..4cffacd 100644
--- a/lib/core/constants/colors.dart
+++ b/lib/core/constants/colors.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
class AppColors {
+ static const Color mainBlue = Color(0xff0164FF);
+ static const Color borderBlue = Color(0xffA1C6FF);
// Primary Colors
static const Color primaryColor = Color(0xFF007BFF);
static const Color accentColor = Color(0xFFE83E8C);
diff --git a/lib/core/constants/texts.dart b/lib/core/constants/texts.dart
new file mode 100644
index 0000000..f336237
--- /dev/null
+++ b/lib/core/constants/texts.dart
@@ -0,0 +1,7 @@
+class TextManager {
+ static const signInTitle = 'Sign In';
+ static const signInSubtitle = 'Enter valid username & password to continue';
+ static const usernameLabel = 'User Name';
+ static const passwordLabel = 'Password';
+ static const loginButton = 'Login';
+}
\ No newline at end of file
diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart
new file mode 100644
index 0000000..7b73246
--- /dev/null
+++ b/lib/home/home_page.dart
@@ -0,0 +1,174 @@
+import 'package:flutter/material.dart';
+
+import '../../bottom_navigation.dart';
+import '../../constants/app_strings.dart';
+import '../../size_config.dart';
+import 'widgets.dart';
+
+class HomePage extends StatelessWidget {
+ const HomePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: _body(),
+ bottomNavigationBar: BottomNavigation(),
+ );
+ }
+
+ SafeArea _body() {
+ return SafeArea(
+ child: ListView(
+ children: [
+ Padding(
+ padding: EdgeInsetsDirectional.fromSTEB(
+ 24.0.w(),
+ 26.0.h(),
+ 24.0.w(),
+ 24.0.h(),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _appbar(),
+ SizedBox(height: 32.0.h()),
+ _progressBarsRow(),
+ SizedBox(height: 32.0.h()),
+ _firstStatisticsRow(),
+ SizedBox(height: 8.0.h()),
+ _secondStatisticsRow(),
+ SizedBox(height: 32.0.h()),
+ _yourTenderText(),
+ SizedBox(height: 28.0.h()),
+ _bottomListView(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _appbar() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ AppStrings.home,
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF222222),
+ ),
+ ),
+ Image.asset(
+ 'assets/icons/tenderLogo.png',
+ width: 59.0.w(),
+ height: 28.0.h(),
+ fit: BoxFit.cover,
+ ),
+ ],
+ );
+ }
+
+ Widget _progressBarsRow() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ ProgressBarColumn(
+ text: AppStrings.partnership,
+ value: 0.75,
+ amount: '75%',
+ ),
+ ProgressBarColumn(
+ text: AppStrings.selfApply,
+ value: 0.88,
+ amount: '12',
+ ),
+ ProgressBarColumn(
+ text: AppStrings.contracting,
+ value: 0.36,
+ amount: '36',
+ ),
+ ],
+ );
+ }
+
+ Widget _firstStatisticsRow() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ StatisticsCard(
+ backgroundColor: Color(0xFFE5EFFF),
+ iconPath: 'assets/icons/arrows.svg',
+ title: AppStrings.tenderSubmitting,
+ amount: '12',
+ textColor: Color(0xFF0164FF),
+ enableTap: true,
+ onTap: () {},
+ ),
+ StatisticsCard(
+ backgroundColor: Color(0xFFEAF8F9),
+ iconPath: 'assets/icons/thumb.svg',
+ title: AppStrings.approvedTenders,
+ amount: '03',
+ textColor: Color(0xFF24848E),
+ enableTap: true,
+ onTap: () {},
+ ),
+ ],
+ );
+ }
+
+ Widget _secondStatisticsRow() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ StatisticsCard(
+ backgroundColor: Color(0xFFFFF2E0),
+ iconPath: 'assets/icons/shield.svg',
+ title: AppStrings.tenderValue,
+ amount: '\$250,000',
+ textColor: Color(0xFFE5821E),
+ enableTap: true,
+ onTap: () {},
+ ),
+ StatisticsCard(
+ backgroundColor: Color(0xFFF4F4F4),
+ iconPath: 'assets/icons/edit.svg',
+ title: AppStrings.thunderStatus,
+ amount: '23',
+ textColor: Color(0xFF9E9E9E),
+ enableTap: false,
+ onTap: () {},
+ ),
+ ],
+ );
+ }
+
+ Widget _yourTenderText() {
+ return Text(
+ AppStrings.yourTenders,
+ style: TextStyle(
+ fontSize: 20.0.sp(),
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF777777),
+ ),
+ );
+ }
+
+ Widget _bottomListView() {
+ return SizedBox(
+ width: double.infinity,
+ height: 310.0.h(),
+ child: ListView.builder(
+ itemCount: 10,
+ scrollDirection: Axis.horizontal,
+ itemBuilder: (context, index) {
+ return TendersListItem();
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/home/progress_bar_column.dart b/lib/home/progress_bar_column.dart
new file mode 100644
index 0000000..e7224ed
--- /dev/null
+++ b/lib/home/progress_bar_column.dart
@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+
+import '../size_config.dart';
+
+class ProgressBarColumn extends StatefulWidget {
+ const ProgressBarColumn({
+ super.key,
+ required this.text,
+ required this.value,
+ required this.amount,
+ });
+
+ final String text;
+ final double value;
+ final String amount;
+
+ @override
+ State createState() => _ProgressBarColumnState();
+}
+
+class _ProgressBarColumnState extends State
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+ late Animation _animation;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Initialize the animation controller
+ _controller = AnimationController(
+ vsync: this,
+ duration: Duration(milliseconds: 1500), // duration of the animation
+ );
+
+ // Create a Tween animation from 0.0 to 0.75
+ _animation = Tween(
+ begin: 0.0,
+ end: widget.value,
+ ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
+
+ // Start the animation
+ _controller.forward();
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose(); // clean up controller
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Stack(
+ alignment: Alignment.center,
+ children: [_progressIndicator(), _centerText()],
+ ),
+ SizedBox(height: 12.0.h()),
+ _titleText(),
+ ],
+ );
+ }
+
+ Widget _progressIndicator() {
+ return SizedBox(
+ width: 96.0.w(),
+ height: 96.0.w(),
+ child: AnimatedBuilder(
+ animation: _animation,
+ builder:
+ (context, child) => CircularProgressIndicator(
+ backgroundColor: Color(0xFFE1E1E1),
+ strokeCap: StrokeCap.round,
+ strokeWidth: 8.0,
+ valueColor: AlwaysStoppedAnimation(Color(0xFF34BCCB)),
+ value: _animation.value,
+ ),
+ ),
+ );
+ }
+
+ Widget _centerText() {
+ return Center(
+ child: Text(
+ widget.amount,
+ style: TextStyle(fontSize: 24.0.sp(), fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ );
+ }
+
+ Widget _titleText() {
+ return Text(
+ widget.text,
+ style: TextStyle(
+ fontSize: 16.0.sp(),
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF222222),
+ ),
+ );
+ }
+}
diff --git a/lib/home/statistics_card.dart b/lib/home/statistics_card.dart
new file mode 100644
index 0000000..eb8a17f
--- /dev/null
+++ b/lib/home/statistics_card.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../size_config.dart';
+
+class StatisticsCard extends StatelessWidget {
+ const StatisticsCard({
+ super.key,
+ required this.backgroundColor,
+ required this.iconPath,
+ required this.title,
+ required this.amount,
+ required this.textColor,
+ required this.onTap,
+ required this.enableTap,
+ });
+
+ final Color backgroundColor;
+ final String iconPath;
+ final String title;
+ final String amount;
+ final Color textColor;
+ final VoidCallback onTap;
+ final bool enableTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: enableTap ? onTap : null,
+ child: Container(
+ width: 178.0.w(),
+ height: 148.0.h(),
+ padding: EdgeInsets.symmetric(horizontal: 16.0.w(), vertical: 12.0.h()),
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SvgPicture.asset(iconPath),
+ SizedBox(height: 4.0.h()),
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 16.0.sp(),
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF777777),
+ ),
+ ),
+ Spacer(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ amount,
+ style: TextStyle(
+ fontSize: 24.0.sp(),
+ fontWeight: FontWeight.bold,
+ color: textColor,
+ ),
+ ),
+ enableTap
+ ? SvgPicture.asset('assets/icons/arrow-right.svg')
+ : SizedBox(),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/home/tenders_list_item.dart b/lib/home/tenders_list_item.dart
new file mode 100644
index 0000000..301cbca
--- /dev/null
+++ b/lib/home/tenders_list_item.dart
@@ -0,0 +1,111 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../size_config.dart';
+
+class TendersListItem extends StatelessWidget {
+ const TendersListItem({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: 327.0.w(),
+ height: 309.0.h(),
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16.0.w(), vertical: 16.0.h()),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '2025-05-21',
+ style: TextStyle(
+ fontSize: 12.0.sp(),
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF777777),
+ ),
+ ),
+ Container(
+ width: 91.0.w(),
+ height: 24.0.h(),
+ decoration: BoxDecoration(
+ color: Color(0xFFDAF8D6),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ alignment: Alignment.center,
+ child: Text(
+ 'Completed',
+ style: TextStyle(
+ fontSize: 12.0.sp(),
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF289744),
+ ),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 12.0.h()),
+ Text(
+ 'Lorem ipsum dolor sit amet consectetur. Sed mi tortor eu purus senectus.',
+ style: TextStyle(
+ fontSize: 16.0.sp(),
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF222222),
+ ),
+ ),
+ SizedBox(height: 8.0.h()),
+ Text(
+ 'Lorem ipsum dolor sit amet consectetur. Volutpat velit tincidunt amet diam. Placerat congue ut Sed facilisis faucibus porttitor. Proin suspendisse eu euismod sit lorem quis. Suscipit ...',
+ style: TextStyle(
+ fontSize: 14.0.sp(),
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF222222),
+ ),
+ ),
+ SizedBox(height: 24.0.h()),
+ Row(
+ children: [
+ SvgPicture.asset('assets/icons/location.svg'),
+ SizedBox(width: 1.0.w()),
+ Text(
+ 'Location',
+ style: TextStyle(
+ fontSize: 12.0.sp(),
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF777777),
+ ),
+ ),
+ SizedBox(width: 8.0.w()),
+ SizedBox(
+ width: 32.0.w(),
+ height: 21.0.h(),
+ child: Image.asset('assets/icons/SE.png', fit: BoxFit.cover),
+ ),
+ Spacer(),
+ Container(
+ width: 96.0.w(),
+ height: 24.0.h(),
+ decoration: BoxDecoration(
+ color: Color(0xFFE4E4E4),
+ borderRadius: BorderRadius.circular(88),
+ border: Border.all(color: Color(0xFFDADADA)),
+ ),
+ alignment: Alignment.center,
+ child: Text(
+ 'Self Control',
+ style: TextStyle(
+ fontSize: 12.0.sp(),
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF777777),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/home/widgets.dart b/lib/home/widgets.dart
new file mode 100644
index 0000000..57c84fa
--- /dev/null
+++ b/lib/home/widgets.dart
@@ -0,0 +1,3 @@
+export 'progress_bar_column.dart';
+export 'statistics_card.dart';
+export 'tenders_list_item.dart';
diff --git a/lib/main.dart b/lib/main.dart
index 585f0c5..a9cfff6 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'data/repositories/auth_repository.dart';
import 'data/services/auth_service.dart';
+import 'size_config.dart';
import 'view_models/auth_view_model.dart';
import 'views/login_screen.dart';
@@ -32,6 +33,13 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return const MaterialApp(home: LoginScreen());
+ SizeConfig.init(context);
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+ ),
+ home: LoginScreen(),
+ );
}
}
diff --git a/lib/size_config.dart b/lib/size_config.dart
new file mode 100644
index 0000000..49c0eeb
--- /dev/null
+++ b/lib/size_config.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+
+class SizeConfig {
+ static double screenWidth = 0.0;
+ static double screenHeight = 0.0;
+ static late TextScaler textScaler;
+
+ static void init(BuildContext context) {
+ screenWidth = MediaQuery.sizeOf(context).width;
+ screenHeight = MediaQuery.sizeOf(context).height;
+ textScaler = MediaQuery.of(context).textScaler;
+ }
+}
+
+extension SizeDoubleExt on double {
+ double h() {
+ final double screenHeight = SizeConfig.screenHeight;
+ final double res = (this / 917.0) * screenHeight;
+ return res;
+ }
+
+ double w() {
+ final double screenWidth = SizeConfig.screenWidth;
+ final double res = (this / 412.0) * screenWidth;
+ return res;
+ }
+
+ double sp() {
+ return h();
+ }
+}
diff --git a/lib/views/login_screen.dart b/lib/views/login_screen.dart
index fbc77cd..fee7a1a 100644
--- a/lib/views/login_screen.dart
+++ b/lib/views/login_screen.dart
@@ -1,8 +1,13 @@
// lib/views/login_screen.dart
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-import '../view_models/auth_view_model.dart';
+import '../core/constants/assets.dart';
+import '../core/constants/texts.dart';
+import '../size_config.dart';
+import '../views/widget/login_button.dart';
+import '../views/widget/login_logo.dart';
+import '../views/widget/login_textfield.dart';
+import '../views/widget/login_title.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -12,58 +17,81 @@ class LoginScreen extends StatefulWidget {
}
class _LoginScreenState extends State {
- final _emailController = TextEditingController();
+ final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
+ final _usernameFocus = FocusNode();
+ final _passwordFocus = FocusNode();
+
+ bool _obscurePassword = true;
+
+ bool get _isLoginEnabled =>
+ _usernameController.text.isNotEmpty &&
+ _passwordController.text.isNotEmpty;
+
+ @override
+ void initState() {
+ super.initState();
+ _usernameController.addListener(_updateState);
+ _passwordController.addListener(_updateState);
+ _usernameFocus.addListener(_updateState);
+ _passwordFocus.addListener(_updateState);
+ }
+
+ @override
+ void dispose() {
+ _usernameController.dispose();
+ _passwordController.dispose();
+ _usernameFocus.dispose();
+ _passwordFocus.dispose();
+ super.dispose();
+ }
+
+ void _updateState() => setState(() {});
+
+ void _togglePasswordVisibility() =>
+ setState(() => _obscurePassword = !_obscurePassword);
+
@override
Widget build(BuildContext context) {
- final authViewModel = Provider.of(context);
-
return Scaffold(
- appBar: AppBar(title: const Text('Login')),
- body: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- children: [
- TextField(
- controller: _emailController,
- decoration: const InputDecoration(labelText: 'Email'),
- ),
- TextField(
- controller: _passwordController,
- decoration: const InputDecoration(labelText: 'Password'),
- obscureText: true,
- ),
- const SizedBox(height: 20),
- if (authViewModel.isLoading)
- const CircularProgressIndicator()
- else
- ElevatedButton(
- onPressed: () {
- authViewModel.login(
- _emailController.text,
- _passwordController.text,
- );
- },
- child: const Text('Login'),
+ backgroundColor: Colors.white,
+ body: Center(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const LoginLogo(),
+ SizedBox(height: 32.0.h()),
+ const LoginTitle(),
+ SizedBox(height: 32.0.h()),
+ LoginTextField(
+ controller: _usernameController,
+ focusNode: _usernameFocus,
+ label: TextManager.usernameLabel,
+ iconPath: AssetsManager.userIcon,
),
- if (authViewModel.errorMessage != null)
- Padding(
- padding: const EdgeInsets.only(top: 20),
- child: Text(
- authViewModel.errorMessage!,
- style: const TextStyle(color: Colors.red),
- ),
+
+ SizedBox(height: 16.0.h()),
+
+ LoginTextField(
+ controller: _passwordController,
+ focusNode: _passwordFocus,
+ label: TextManager.passwordLabel,
+ iconPath: AssetsManager.passwordIcon,
+ isPassword: true,
+ obscureText: _obscurePassword,
+ onToggleVisibility: _togglePasswordVisibility,
),
- if (authViewModel.loggedInUser != null)
- Padding(
- padding: const EdgeInsets.only(top: 20),
- child: Text(
- 'Welcome, ${authViewModel.loggedInUser!.name}!',
- style: const TextStyle(color: Colors.green),
- ),
+
+ SizedBox(height: 32.0.h()),
+ LoginButton(
+ isEnabled: _isLoginEnabled,
+ onPressed: _isLoginEnabled ? () {} : null,
),
- ],
+ ],
+ ),
),
),
);
diff --git a/lib/views/widget/login_button.dart b/lib/views/widget/login_button.dart
new file mode 100644
index 0000000..e954b43
--- /dev/null
+++ b/lib/views/widget/login_button.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+
+import '../../core/constants/colors.dart';
+import '../../core/constants/texts.dart';
+import '../../size_config.dart';
+
+class LoginButton extends StatelessWidget {
+ final bool isEnabled;
+ final VoidCallback? onPressed;
+
+ const LoginButton({
+ super.key,
+ required this.isEnabled,
+ required this.onPressed,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: double.infinity,
+ height: 55.0.h(),
+ child: ElevatedButton(
+ onPressed: onPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: isEnabled ? ColorManager.mainBlue : Colors.grey[300],
+ foregroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(28.0.w()),
+ ),
+ ),
+ child: Text(
+ TextManager.loginButton,
+ style: TextStyle(fontSize: 16.0.sp(), fontWeight: FontWeight.bold),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/views/widget/login_logo.dart b/lib/views/widget/login_logo.dart
new file mode 100644
index 0000000..6458770
--- /dev/null
+++ b/lib/views/widget/login_logo.dart
@@ -0,0 +1,13 @@
+import 'package:flutter/material.dart';
+
+import '../../core/constants/assets.dart';
+import '../../size_config.dart';
+
+class LoginLogo extends StatelessWidget {
+ const LoginLogo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Image.asset(AssetsManager.logo, width: 190.0.w(), height: 88.0.h());
+ }
+}
diff --git a/lib/views/widget/login_textfield.dart b/lib/views/widget/login_textfield.dart
new file mode 100644
index 0000000..872259f
--- /dev/null
+++ b/lib/views/widget/login_textfield.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../../core/constants/assets.dart';
+import '../../core/constants/colors.dart';
+import '../../size_config.dart';
+
+class LoginTextField extends StatelessWidget {
+ final TextEditingController controller;
+ final FocusNode focusNode;
+ final String label;
+ final String iconPath;
+ final bool isPassword;
+ final bool obscureText;
+ final VoidCallback? onToggleVisibility;
+
+ const LoginTextField({
+ super.key,
+ required this.controller,
+ required this.focusNode,
+ required this.label,
+ required this.iconPath,
+ this.isPassword = false,
+ this.obscureText = false,
+ this.onToggleVisibility,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ controller: controller,
+ focusNode: focusNode,
+ obscureText: isPassword ? obscureText : false,
+ decoration: InputDecoration(
+ labelText: label,
+ prefixIcon: Padding(
+ padding: EdgeInsets.all(12.0.w()),
+ child: SvgPicture.asset(
+ iconPath,
+ width: 24.0.w(),
+ height: 24.0.h(),
+ colorFilter: ColorFilter.mode(
+ focusNode.hasFocus ? Colors.black87 : Colors.grey,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ suffixIcon:
+ isPassword
+ ? IconButton(
+ onPressed: onToggleVisibility,
+ icon: SvgPicture.asset(
+ obscureText
+ ? AssetsManager.passwordVisibility
+ : AssetsManager.passwordVisibilityOff,
+ width: 24.0.w(),
+ height: 24.0.h(),
+ colorFilter: ColorFilter.mode(
+ focusNode.hasFocus ? Colors.black87 : Colors.grey,
+ BlendMode.srcIn,
+ ),
+ ),
+ )
+ : null,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12.0.w()),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: ColorManager.borderBlue),
+ borderRadius: BorderRadius.circular(12.0.w()),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/views/widget/login_title.dart b/lib/views/widget/login_title.dart
new file mode 100644
index 0000000..ed456d3
--- /dev/null
+++ b/lib/views/widget/login_title.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+
+import '../../core/constants/texts.dart';
+import '../../size_config.dart';
+
+class LoginTitle extends StatelessWidget {
+ const LoginTitle({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Text(
+ TextManager.signInTitle,
+ style: TextStyle(fontSize: 28.0.sp(), fontWeight: FontWeight.w700),
+ ),
+ SizedBox(height: 8.0.h()),
+ Text(
+ TextManager.signInSubtitle,
+ style: TextStyle(
+ fontSize: 14.0.sp(),
+ fontWeight: FontWeight.w400,
+ color: Colors.grey,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 55c68bf..804551f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,6 +16,12 @@ environment:
dependencies:
flutter:
sdk: flutter
+
+ provider: ^6.1.5
+ flutter_svg: ^2.2.0
+ freezed: ^3.1.0
+ freezed_annotation: ^3.1.0
+
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@@ -48,8 +54,10 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
- # assets:
- # - images/a_dot_burr.jpeg
+ assets:
+ - assets/icons/
+ - assets/svgs/
+ - assets/pngs/
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see