Skip to content

Commit 18d3e82

Browse files
committed
feat(auth): build complete auth UI screens (Login, Register, OTP, Passwords)
1 parent 63f3e9e commit 18d3e82

5 files changed

Lines changed: 580 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import 'package:flutter/material.dart';
2+
import '../../core/theme/app_colors.dart';
3+
import '../../core/theme/auth_layout_template.dart';
4+
import '../../core/theme/custom_text_field.dart';
5+
import '../../viewmodels/auth_viewmodels.dart';
6+
import 'otp_verification_view.dart';
7+
8+
class ForgotPasswordView extends StatefulWidget {
9+
const ForgotPasswordView({super.key});
10+
11+
@override
12+
State<ForgotPasswordView> createState() => _ForgotPasswordViewState();
13+
}
14+
15+
class _ForgotPasswordViewState extends State<ForgotPasswordView> {
16+
final _vm = ForgotPassViewModel();
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return AnimatedBuilder(
21+
animation: _vm,
22+
builder: (context, _) => AuthLayoutTemplate(
23+
title: 'Quên mật khẩu?',
24+
subtitle: 'Nhập email của bạn để nhận mã xác thực',
25+
submitText: 'Gửi mã',
26+
useCard: false,
27+
isLoading: _vm.isLoading,
28+
customHeaderIcon: Container(
29+
width: 120,
30+
height: 120,
31+
decoration: const BoxDecoration(
32+
color: AppColors.white,
33+
shape: BoxShape.circle,
34+
boxShadow: [BoxShadow(color: Color(0xFFD1D9E6), blurRadius: 16)],
35+
),
36+
child: const Icon(
37+
Icons.lock_reset,
38+
size: 64,
39+
color: AppColors.primary,
40+
),
41+
),
42+
onSubmit: () async {
43+
FocusScope.of(context).unfocus();
44+
final success = await _vm.sendCode();
45+
if (!context.mounted) return;
46+
47+
if (success) {
48+
// Jump to Step 2: OTP
49+
Navigator.push(
50+
context,
51+
MaterialPageRoute(builder: (_) => const OtpVerificationView()),
52+
);
53+
} else {
54+
ScaffoldMessenger.of(context).showSnackBar(
55+
const SnackBar(
56+
content: Text('Vui lòng nhập email hợp lệ!'),
57+
backgroundColor: AppColors.error,
58+
),
59+
);
60+
}
61+
},
62+
formContent: CustomTextField(
63+
label: 'Địa chỉ Email',
64+
65+
icon: Icons.mail,
66+
controller: _vm.emailCtrl,
67+
),
68+
footerContent: const Row(
69+
mainAxisAlignment: MainAxisAlignment.center,
70+
children: [
71+
Icon(Icons.help, size: 16, color: AppColors.primary),
72+
SizedBox(width: 4),
73+
Text(
74+
'Cần hỗ trợ? Liên hệ CSKH',
75+
style: TextStyle(
76+
color: AppColors.textSecondary,
77+
fontSize: 12,
78+
fontWeight: FontWeight.bold,
79+
),
80+
),
81+
],
82+
),
83+
),
84+
);
85+
}
86+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'package:flutter/material.dart';
2+
import '../../core/theme/app_colors.dart';
3+
import '../../core/theme/auth_layout_template.dart';
4+
import '../../core/theme/custom_text_field.dart';
5+
import '../../viewmodels/auth_viewmodels.dart';
6+
import 'register_view.dart';
7+
import 'forgot_password_view.dart';
8+
9+
class LoginView extends StatefulWidget {
10+
const LoginView({super.key});
11+
12+
@override
13+
State<LoginView> createState() => _LoginViewState();
14+
}
15+
16+
class _LoginViewState extends State<LoginView> {
17+
final _vm = LoginViewModel();
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
return AnimatedBuilder(
22+
animation: _vm,
23+
builder: (context, _) =>
24+
AuthLayoutTemplate(
25+
title: 'Task Management',
26+
subtitle: 'Chào mừng trở lại!',
27+
submitText: 'Đăng nhập',
28+
isLoading: _vm.isLoading,
29+
showSocial: true,
30+
onSubmit: () async {
31+
FocusScope.of(context).unfocus(); // Hide keyboard
32+
final success = await _vm.login();
33+
if (!context.mounted) return;
34+
35+
if (success) {
36+
ScaffoldMessenger.of(context).showSnackBar(
37+
const SnackBar(
38+
content: Text('Đăng nhập thành công!'),
39+
backgroundColor: AppColors.success,
40+
),
41+
);
42+
// TODO: Navigator.pushReplacementNamed(context, '/home');
43+
} else {
44+
ScaffoldMessenger.of(context).showSnackBar(
45+
const SnackBar(
46+
content: Text('Vui lòng điền đầy đủ thông tin!'),
47+
backgroundColor: AppColors.error,
48+
),
49+
);
50+
}
51+
},
52+
formContent: Column(
53+
crossAxisAlignment: CrossAxisAlignment.end,
54+
children: [
55+
CustomTextField(
56+
label: 'Email',
57+
58+
icon: Icons.mail,
59+
controller: _vm.emailCtrl,
60+
),
61+
CustomTextField(
62+
label: 'Mật khẩu',
63+
hint: '••••••••',
64+
icon: Icons.lock,
65+
controller: _vm.passCtrl,
66+
isPassword: true,
67+
obscureText: _vm.obscurePass,
68+
onToggleVisibility: _vm.togglePass,
69+
),
70+
TextButton(
71+
onPressed: () =>
72+
Navigator.push(
73+
context,
74+
MaterialPageRoute(
75+
builder: (_) => const ForgotPasswordView()),
76+
),
77+
child: const Text(
78+
'Quên mật khẩu?',
79+
style: TextStyle(
80+
color: AppColors.primary,
81+
fontWeight: FontWeight.bold,
82+
),
83+
),
84+
),
85+
],
86+
),
87+
footerContent: _buildFooter(
88+
'Chưa có tài khoản? ', 'Đăng ký ngay', () {
89+
Navigator.push(
90+
context,
91+
MaterialPageRoute(builder: (_) => const RegisterView()),
92+
);
93+
}),
94+
),
95+
);
96+
}
97+
98+
Widget _buildFooter(String text, String action, VoidCallback onTap) {
99+
return Padding(
100+
// Position this block 24dp above the bottom of the screen
101+
padding: const EdgeInsets.only(bottom: 24.0),
102+
child: Row(
103+
mainAxisAlignment: MainAxisAlignment.center,
104+
children: [
105+
Text(text, style: const TextStyle(color: Colors.grey, fontSize: 16)),
106+
107+
TextButton(
108+
onPressed: onTap,
109+
style: TextButton.styleFrom(
110+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
111+
minimumSize: const Size(50, 48),
112+
),
113+
child: Text(
114+
action,
115+
style: const TextStyle(color: Color(0xFF5A8DF3),
116+
fontWeight: FontWeight.bold,
117+
fontSize: 16),
118+
),
119+
),
120+
],
121+
),
122+
);
123+
}
124+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:flutter/material.dart';
2+
import '../../core/theme/app_colors.dart';
3+
import '../../core/theme/auth_layout_template.dart';
4+
import '../../core/theme/custom_text_field.dart';
5+
import '../../viewmodels/auth_viewmodels.dart';
6+
7+
class NewPasswordView extends StatefulWidget {
8+
const NewPasswordView({super.key});
9+
10+
@override
11+
State<NewPasswordView> createState() => _NewPasswordViewState();
12+
}
13+
14+
class _NewPasswordViewState extends State<NewPasswordView> {
15+
final _vm = NewPassViewModel();
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
return AnimatedBuilder(
20+
animation: _vm,
21+
builder: (context, _) => AuthLayoutTemplate(
22+
title: 'Tạo mật khẩu mới',
23+
subtitle: 'Mật khẩu mới phải khác với mật khẩu cũ',
24+
submitText: 'Cập nhật',
25+
isLoading: _vm.isLoading,
26+
customHeaderIcon: const CircleAvatar(
27+
radius: 40,
28+
backgroundColor: Color(0xFFEBF2FF),
29+
child: Icon(Icons.lock_reset, size: 40, color: AppColors.primary),
30+
),
31+
onSubmit: () async {
32+
FocusScope.of(context).unfocus();
33+
final success = await _vm.updatePassword();
34+
if (!context.mounted) return;
35+
36+
if (success) {
37+
ScaffoldMessenger.of(context).showSnackBar(
38+
const SnackBar(
39+
content: Text('Đổi mật khẩu thành công!'),
40+
backgroundColor: AppColors.success,
41+
),
42+
);
43+
// Complete Flow: Pop everything and return to Login Screen
44+
Navigator.popUntil(context, (route) => route.isFirst);
45+
} else {
46+
ScaffoldMessenger.of(context).showSnackBar(
47+
const SnackBar(
48+
content: Text('Mật khẩu không khớp!'),
49+
backgroundColor: AppColors.error,
50+
),
51+
);
52+
}
53+
},
54+
formContent: Column(
55+
children: [
56+
CustomTextField(
57+
label: 'Mật khẩu mới',
58+
hint: '••••••••',
59+
icon: Icons.lock,
60+
controller: _vm.passCtrl,
61+
isPassword: true,
62+
obscureText: _vm.obscurePass,
63+
onToggleVisibility: _vm.togglePass,
64+
),
65+
CustomTextField(
66+
label: 'Xác nhận mật khẩu mới',
67+
hint: '••••••••',
68+
icon: Icons.lock,
69+
controller: _vm.confirmPassCtrl,
70+
isPassword: true,
71+
obscureText: _vm.obscurePass,
72+
onToggleVisibility: _vm.togglePass,
73+
),
74+
Container(
75+
padding: const EdgeInsets.all(12),
76+
decoration: BoxDecoration(
77+
color: const Color(0xFFEBF2FF),
78+
borderRadius: BorderRadius.circular(12),
79+
),
80+
child: const Row(
81+
children: [
82+
Icon(Icons.info, color: AppColors.primary, size: 16),
83+
SizedBox(width: 8),
84+
Expanded(
85+
child: Text(
86+
'Mật khẩu tối thiểu 8 ký tự để đảm bảo an toàn.',
87+
style: TextStyle(fontSize: 12, color: AppColors.primary),
88+
),
89+
),
90+
],
91+
),
92+
),
93+
],
94+
),
95+
),
96+
);
97+
}
98+
}

0 commit comments

Comments
 (0)