Skip to content

Commit a82c86a

Browse files
authored
feat: implement complete auth flow with MVVM architecture (#11)
* feat(core): add auth layout template, custom textfield and colors * feat(auth): implement viewmodels for auth flow (MVVM) * feat(auth): build complete auth UI screens (Login, Register, OTP, Passwords) * chore(main): set LoginView as initial route * refactor(auth) : delete .gitkeep
1 parent 388c27c commit a82c86a

11 files changed

Lines changed: 1003 additions & 4 deletions

src/lib/core/theme/app_colors.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@ class AppColors {
77
static const Color taskCardBg = Colors.white;
88
static const Color timeBoxBg = Color(0xFF2C3E50);
99
static const Color grayText = Color(0xFF757575);
10-
}
10+
static const Color primary = Color(0xFF5A8DF3);
11+
static const Color background = Color(0xFFF4F6F9);
12+
static const Color textDark = Color(0xFF2D3440);
13+
static const Color textSecondary = Colors.grey;
14+
static const Color inputBackground = Color(0xFFF8FAFC);
15+
static const Color border = Color(0xFFE2E8F0);
16+
static const Color white = Colors.white;
17+
static const Color error = Colors.redAccent;
18+
static const Color success = Colors.green;
19+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import 'package:flutter/material.dart';
2+
import 'app_colors.dart';
3+
4+
/// The master layout template for all authentication screens.
5+
/// Handles the UI skeleton, loading states, backgrounds, and responsive scrolling.
6+
class AuthLayoutTemplate extends StatelessWidget {
7+
final String title;
8+
final String subtitle;
9+
final Widget formContent;
10+
final String submitText;
11+
final VoidCallback onSubmit;
12+
final bool isLoading;
13+
final bool showSocial;
14+
final bool useCard;
15+
final Widget? customHeaderIcon;
16+
final Widget? footerContent;
17+
18+
const AuthLayoutTemplate({
19+
super.key,
20+
required this.title,
21+
required this.subtitle,
22+
required this.formContent,
23+
required this.submitText,
24+
required this.onSubmit,
25+
this.isLoading = false,
26+
this.showSocial = false,
27+
this.useCard = true,
28+
this.customHeaderIcon,
29+
this.footerContent,
30+
});
31+
32+
@override
33+
Widget build(BuildContext context) {
34+
return Scaffold(
35+
backgroundColor: AppColors.background,
36+
appBar: AppBar(
37+
backgroundColor: Colors.transparent,
38+
elevation: 0,
39+
leading: Navigator.canPop(context)
40+
? IconButton(
41+
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
42+
onPressed: () => Navigator.pop(context),
43+
)
44+
: null,
45+
),
46+
body: SafeArea(
47+
child: Center(
48+
child: SingleChildScrollView(
49+
padding: const EdgeInsets.fromLTRB(24.0, 16.0, 24.0, 48.0),
50+
child: Column(
51+
mainAxisAlignment: MainAxisAlignment.center,
52+
children: [
53+
_buildHeader(),
54+
const SizedBox(height: 32),
55+
useCard ? _buildCardContainer() : _buildTransparentContainer(),
56+
const SizedBox(height: 32),
57+
if (footerContent != null) footerContent!,
58+
],
59+
),
60+
),
61+
),
62+
),
63+
);
64+
}
65+
66+
Widget _buildHeader() {
67+
return Column(
68+
children: [
69+
customHeaderIcon ??
70+
Container(
71+
width: 80,
72+
height: 80,
73+
decoration: BoxDecoration(
74+
color: AppColors.white,
75+
borderRadius: BorderRadius.circular(24),
76+
boxShadow: [
77+
BoxShadow(
78+
color: AppColors.primary.withOpacity(0.1),
79+
blurRadius: 20,
80+
offset: const Offset(0, 10),
81+
),
82+
],
83+
),
84+
child: const Center(
85+
child: Icon(Icons.task_alt, size: 48, color: AppColors.primary),
86+
),
87+
),
88+
const SizedBox(height: 24),
89+
Text(
90+
title,
91+
style: const TextStyle(
92+
fontSize: 28,
93+
fontWeight: FontWeight.w800,
94+
color: AppColors.textDark,
95+
letterSpacing: -0.5,
96+
),
97+
),
98+
const SizedBox(height: 8),
99+
Text(
100+
subtitle,
101+
style: const TextStyle(
102+
fontSize: 14,
103+
fontWeight: FontWeight.w500,
104+
color: AppColors.textSecondary,
105+
),
106+
textAlign: TextAlign.center,
107+
),
108+
],
109+
);
110+
}
111+
112+
Widget _buildCardContainer() {
113+
return Container(
114+
padding: const EdgeInsets.all(32),
115+
decoration: BoxDecoration(
116+
color: AppColors.white,
117+
borderRadius: BorderRadius.circular(32),
118+
boxShadow: [
119+
BoxShadow(
120+
color: AppColors.primary.withOpacity(0.08),
121+
blurRadius: 30,
122+
offset: const Offset(0, 10),
123+
),
124+
],
125+
),
126+
child: _buildFormElements(),
127+
);
128+
}
129+
130+
Widget _buildTransparentContainer() => _buildFormElements();
131+
132+
Widget _buildFormElements() {
133+
return Column(
134+
crossAxisAlignment: CrossAxisAlignment.stretch,
135+
children: [
136+
formContent,
137+
const SizedBox(height: 16),
138+
ElevatedButton(
139+
// Disable button if loading
140+
onPressed: isLoading ? null : onSubmit,
141+
style: ElevatedButton.styleFrom(
142+
backgroundColor: AppColors.primary,
143+
disabledBackgroundColor: AppColors.primary.withOpacity(0.6),
144+
padding: const EdgeInsets.symmetric(vertical: 20),
145+
shape: RoundedRectangleBorder(
146+
borderRadius: BorderRadius.circular(16),
147+
),
148+
elevation: isLoading ? 0 : 4,
149+
),
150+
child: isLoading
151+
? const SizedBox(
152+
height: 20,
153+
width: 20,
154+
child: CircularProgressIndicator(
155+
color: AppColors.white,
156+
strokeWidth: 2,
157+
),
158+
)
159+
: Row(
160+
mainAxisAlignment: MainAxisAlignment.center,
161+
children: [
162+
Text(
163+
submitText,
164+
style: const TextStyle(
165+
fontSize: 16,
166+
fontWeight: FontWeight.bold,
167+
color: AppColors.white,
168+
),
169+
),
170+
const SizedBox(width: 8),
171+
const Icon(
172+
Icons.arrow_forward,
173+
color: AppColors.white,
174+
size: 20,
175+
),
176+
],
177+
),
178+
),
179+
if (showSocial) ...[
180+
const SizedBox(height: 32),
181+
const Row(
182+
children: [
183+
Expanded(child: Divider(color: AppColors.border)),
184+
Padding(
185+
padding: EdgeInsets.symmetric(horizontal: 16),
186+
child: Text(
187+
'OR',
188+
style: TextStyle(
189+
fontSize: 10,
190+
fontWeight: FontWeight.bold,
191+
color: AppColors.textSecondary,
192+
),
193+
),
194+
),
195+
Expanded(child: Divider(color: AppColors.border)),
196+
],
197+
),
198+
const SizedBox(height: 24),
199+
Row(
200+
children: [
201+
Expanded(
202+
child: OutlinedButton.icon(
203+
onPressed: () {},
204+
icon: const Icon(
205+
Icons.g_mobiledata,
206+
color: Colors.red,
207+
size: 28,
208+
),
209+
label: const Text('Google'),
210+
),
211+
),
212+
const SizedBox(width: 16),
213+
Expanded(
214+
child: OutlinedButton.icon(
215+
onPressed: () {},
216+
icon: const Icon(Icons.facebook, color: Color(0xFF1877F2)),
217+
label: const Text('Facebook'),
218+
),
219+
),
220+
],
221+
),
222+
],
223+
],
224+
);
225+
}
226+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'package:flutter/material.dart';
2+
import 'app_colors.dart';
3+
4+
/// A highly reusable text input field component.
5+
class CustomTextField extends StatelessWidget {
6+
final String label;
7+
final String hint;
8+
final IconData icon;
9+
final TextEditingController controller;
10+
final bool isPassword;
11+
final bool obscureText;
12+
final VoidCallback? onToggleVisibility;
13+
14+
const CustomTextField({
15+
super.key,
16+
required this.label,
17+
required this.hint,
18+
required this.icon,
19+
required this.controller,
20+
this.isPassword = false,
21+
this.obscureText = false,
22+
this.onToggleVisibility,
23+
});
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
return Padding(
28+
padding: const EdgeInsets.only(bottom: 16),
29+
child: Column(
30+
crossAxisAlignment: CrossAxisAlignment.start,
31+
children: [
32+
Padding(
33+
padding: const EdgeInsets.only(left: 4, bottom: 8),
34+
child: Text(
35+
label.toUpperCase(),
36+
style: TextStyle(
37+
fontSize: 14,
38+
fontWeight: FontWeight.bold,
39+
color: AppColors.textDark.withOpacity(0.6),
40+
letterSpacing: 1,
41+
),
42+
),
43+
),
44+
TextFormField(
45+
controller: controller,
46+
obscureText: obscureText,
47+
style: const TextStyle(
48+
color: AppColors.textDark,
49+
fontWeight: FontWeight.w600,
50+
fontSize: 16,
51+
),
52+
decoration: InputDecoration(
53+
hintText: hint,
54+
hintStyle: TextStyle(
55+
color: AppColors.textDark.withOpacity(0.3),
56+
fontWeight: FontWeight.w400,
57+
fontSize: 16,
58+
),
59+
filled: true,
60+
fillColor: AppColors.inputBackground,
61+
prefixIcon: Icon(icon, color: AppColors.primary),
62+
suffixIcon: isPassword
63+
? IconButton(
64+
icon: Icon(
65+
obscureText ? Icons.visibility_off : Icons.visibility,
66+
color: Colors.grey,
67+
),
68+
onPressed: onToggleVisibility,
69+
)
70+
: null,
71+
contentPadding: const EdgeInsets.symmetric(
72+
vertical: 20,
73+
horizontal: 16,
74+
),
75+
border: OutlineInputBorder(
76+
borderRadius: BorderRadius.circular(16),
77+
borderSide: BorderSide.none,
78+
),
79+
enabledBorder: OutlineInputBorder(
80+
borderRadius: BorderRadius.circular(16),
81+
borderSide: const BorderSide(color: AppColors.border),
82+
),
83+
focusedBorder: OutlineInputBorder(
84+
borderRadius: BorderRadius.circular(16),
85+
borderSide: const BorderSide(
86+
color: AppColors.primary,
87+
width: 2,
88+
),
89+
),
90+
),
91+
),
92+
],
93+
),
94+
);
95+
}
96+
}

src/lib/features/auth/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)