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..0ebd88f --- /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 ? AppColors.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..7825418 --- /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: AppColors.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