merged login and home

This commit is contained in:
amirrezaghabeli
2025-08-03 09:44:43 +03:30
parent 24e14b325a
commit 7aa082c6c8
40 changed files with 979 additions and 49 deletions
+97
View File
@@ -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<BottomNavigation> createState() => _BottomNavigationState();
}
class _BottomNavigationState extends State<BottomNavigation> {
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),
),
),
],
),
),
),
);
}
}
+14
View File
@@ -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';
}
+10
View File
@@ -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';
}
+2
View File
@@ -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);
+7
View File
@@ -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';
}
+174
View File
@@ -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();
},
),
);
}
}
+104
View File
@@ -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<ProgressBarColumn> createState() => _ProgressBarColumnState();
}
class _ProgressBarColumnState extends State<ProgressBarColumn>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _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<double>(
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>(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),
),
);
}
}
+73
View File
@@ -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(),
],
),
],
),
),
);
}
}
+111
View File
@@ -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),
),
),
),
],
),
],
),
),
);
}
}
+3
View File
@@ -0,0 +1,3 @@
export 'progress_bar_column.dart';
export 'statistics_card.dart';
export 'tenders_list_item.dart';
+9 -1
View File
@@ -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(),
);
}
}
+31
View File
@@ -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();
}
}
+74 -46
View File
@@ -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<LoginScreen> {
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<AuthViewModel>(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,
),
],
],
),
),
),
);
+38
View File
@@ -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),
),
),
);
}
}
+13
View File
@@ -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());
}
}
+75
View File
@@ -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()),
),
),
);
}
}
+30
View File
@@ -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,
),
],
);
}
}