feat: login page
This commit is contained in:
parent
afc8476701
commit
318d22d7c9
@ -7,21 +7,21 @@ class AppStyle {
|
|||||||
|
|
||||||
static TextStyle md = TextStyle(color: AppColor.textPrimary, fontSize: 14);
|
static TextStyle md = TextStyle(color: AppColor.textPrimary, fontSize: 14);
|
||||||
|
|
||||||
static TextStyle lg = TextStyle(color: AppColor.textPrimary, fontSize: 18);
|
static TextStyle lg = TextStyle(color: AppColor.textPrimary, fontSize: 16);
|
||||||
|
|
||||||
static TextStyle xl = TextStyle(color: AppColor.textPrimary, fontSize: 20);
|
static TextStyle xl = TextStyle(color: AppColor.textPrimary, fontSize: 18);
|
||||||
|
|
||||||
static TextStyle xxl = TextStyle(color: AppColor.textPrimary, fontSize: 22);
|
static TextStyle xxl = TextStyle(color: AppColor.textPrimary, fontSize: 20);
|
||||||
|
|
||||||
static TextStyle h6 = TextStyle(color: AppColor.textPrimary, fontSize: 24);
|
static TextStyle h6 = TextStyle(color: AppColor.textPrimary, fontSize: 22);
|
||||||
|
|
||||||
static TextStyle h5 = TextStyle(color: AppColor.textPrimary, fontSize: 26);
|
static TextStyle h5 = TextStyle(color: AppColor.textPrimary, fontSize: 24);
|
||||||
|
|
||||||
static TextStyle h4 = TextStyle(color: AppColor.textPrimary, fontSize: 28);
|
static TextStyle h4 = TextStyle(color: AppColor.textPrimary, fontSize: 26);
|
||||||
|
|
||||||
static TextStyle h3 = TextStyle(color: AppColor.textPrimary, fontSize: 30);
|
static TextStyle h3 = TextStyle(color: AppColor.textPrimary, fontSize: 28);
|
||||||
|
|
||||||
static TextStyle h2 = TextStyle(color: AppColor.textPrimary, fontSize: 32);
|
static TextStyle h2 = TextStyle(color: AppColor.textPrimary, fontSize: 30);
|
||||||
|
|
||||||
static TextStyle h1 = TextStyle(color: AppColor.textPrimary, fontSize: 34);
|
static TextStyle h1 = TextStyle(color: AppColor.textPrimary, fontSize: 32);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ part of 'theme.dart';
|
|||||||
|
|
||||||
class AppValue {
|
class AppValue {
|
||||||
static const double padding = 16.0;
|
static const double padding = 16.0;
|
||||||
|
static const double margin = 16.0;
|
||||||
static const double radius = 8.0;
|
static const double radius = 8.0;
|
||||||
static const double elevation = 4.0;
|
static const double elevation = 4.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../presentation/components/assets/fonts.gen.dart';
|
||||||
|
|
||||||
part 'app_color.dart';
|
part 'app_color.dart';
|
||||||
part 'app_style.dart';
|
part 'app_style.dart';
|
||||||
part 'app_value.dart';
|
part 'app_value.dart';
|
||||||
@ -7,5 +9,38 @@ part 'app_value.dart';
|
|||||||
class ThemeApp {
|
class ThemeApp {
|
||||||
static ThemeData get theme => ThemeData(
|
static ThemeData get theme => ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
scaffoldBackgroundColor: AppColor.background,
|
||||||
|
fontFamily: FontFamily.quicksand,
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
hintStyle: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: AppValue.padding),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppValue.radius),
|
||||||
|
borderSide: const BorderSide(color: AppColor.border),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppValue.radius),
|
||||||
|
borderSide: const BorderSide(color: AppColor.border),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppValue.radius),
|
||||||
|
borderSide: const BorderSide(color: AppColor.primary, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppValue.radius),
|
||||||
|
borderSide: const BorderSide(color: AppColor.error),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppColor.backgroundLight,
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppValue.radius),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
28
lib/common/validator/validator.dart
Normal file
28
lib/common/validator/validator.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
class AppValidator {
|
||||||
|
static String? validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Email wajib diisi';
|
||||||
|
}
|
||||||
|
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||||
|
if (!emailRegex.hasMatch(value)) {
|
||||||
|
return 'Format email tidak valid';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Password wajib diisi';
|
||||||
|
}
|
||||||
|
if (value.length < 8) {
|
||||||
|
return 'Password minimal 8 karakter';
|
||||||
|
}
|
||||||
|
// if (!RegExp(r'[A-Z]').hasMatch(value)) {
|
||||||
|
// return 'Password harus mengandung huruf besar';
|
||||||
|
// }
|
||||||
|
// if (!RegExp(r'[0-9]').hasMatch(value)) {
|
||||||
|
// return 'Password harus mengandung angka';
|
||||||
|
// }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib/presentation/components/button/button.dart
Normal file
7
lib/presentation/components/button/button.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
|
||||||
|
import '../../../common/theme/theme.dart';
|
||||||
|
import '../spacer/spacer.dart';
|
||||||
|
|
||||||
|
part 'elevated_button.dart';
|
||||||
67
lib/presentation/components/button/elevated_button.dart
Normal file
67
lib/presentation/components/button/elevated_button.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
part of 'button.dart';
|
||||||
|
|
||||||
|
class AppElevatedButton extends StatelessWidget {
|
||||||
|
const AppElevatedButton({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
required this.isLoading,
|
||||||
|
required this.onPressed,
|
||||||
|
this.height = 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final bool isLoading;
|
||||||
|
final Function()? onPressed;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(colors: AppColor.primaryGradient),
|
||||||
|
borderRadius: BorderRadius.circular(AppValue.radius),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.primaryWithOpacity(0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: isLoading
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Loading',
|
||||||
|
style: AppStyle.lg.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceWidth(8),
|
||||||
|
SpinKitCircle(color: AppColor.white, size: 24.0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
text,
|
||||||
|
style: AppStyle.lg.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/presentation/components/field/field.dart
Normal file
9
lib/presentation/components/field/field.dart
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:line_icons/line_icon.dart';
|
||||||
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
|
import '../../../common/theme/theme.dart';
|
||||||
|
import '../spacer/spacer.dart';
|
||||||
|
|
||||||
|
part 'password_text_form_field.dart';
|
||||||
|
part 'text_form_field.dart';
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
part of 'field.dart';
|
||||||
|
|
||||||
|
class AppPasswordTextFormField extends StatefulWidget {
|
||||||
|
const AppPasswordTextFormField({
|
||||||
|
super.key,
|
||||||
|
this.controller,
|
||||||
|
required this.title,
|
||||||
|
this.hintText,
|
||||||
|
required this.prefixIcon,
|
||||||
|
this.validator,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController? controller;
|
||||||
|
final String title;
|
||||||
|
final String? hintText;
|
||||||
|
final IconData prefixIcon;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppPasswordTextFormField> createState() =>
|
||||||
|
_AppPasswordTextFormFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppPasswordTextFormFieldState extends State<AppPasswordTextFormField> {
|
||||||
|
bool _obscurePassword = true;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: AppStyle.lg.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SpaceHeight(8),
|
||||||
|
TextFormField(
|
||||||
|
controller: widget.controller,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
cursorColor: AppColor.primary,
|
||||||
|
obscureText: _obscurePassword,
|
||||||
|
style: AppStyle.md.copyWith(color: AppColor.textPrimary),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: widget.hintText,
|
||||||
|
prefixIcon: LineIcon(
|
||||||
|
widget.prefixIcon,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePassword ? LineIcons.eye : LineIcons.eyeSlash,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_obscurePassword = !_obscurePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: widget.validator,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
lib/presentation/components/field/text_form_field.dart
Normal file
46
lib/presentation/components/field/text_form_field.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
part of 'field.dart';
|
||||||
|
|
||||||
|
class AppTextFormField extends StatelessWidget {
|
||||||
|
const AppTextFormField({
|
||||||
|
super.key,
|
||||||
|
this.controller,
|
||||||
|
required this.title,
|
||||||
|
this.hintText,
|
||||||
|
required this.prefixIcon,
|
||||||
|
this.validator,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController? controller;
|
||||||
|
final String title;
|
||||||
|
final String? hintText;
|
||||||
|
final IconData prefixIcon;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: AppStyle.lg.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SpaceHeight(8),
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
cursorColor: AppColor.primary,
|
||||||
|
style: AppStyle.md.copyWith(color: AppColor.textPrimary),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
prefixIcon: LineIcon(prefixIcon, color: AppColor.textSecondary),
|
||||||
|
),
|
||||||
|
validator: validator,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/presentation/components/spacer/spacer.dart
Normal file
23
lib/presentation/components/spacer/spacer.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SpaceHeight extends StatelessWidget {
|
||||||
|
const SpaceHeight(this.height, {super.key});
|
||||||
|
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(height: height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpaceWidth extends StatelessWidget {
|
||||||
|
const SpaceWidth(this.width, {super.key});
|
||||||
|
|
||||||
|
final double width;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(width: width);
|
||||||
|
}
|
||||||
|
}
|
||||||
217
lib/presentation/pages/auth/login/login_page.dart
Normal file
217
lib/presentation/pages/auth/login/login_page.dart
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../common/theme/theme.dart';
|
||||||
|
import '../../../components/button/button.dart';
|
||||||
|
import '../../../components/spacer/spacer.dart';
|
||||||
|
import 'widgets/email_field.dart';
|
||||||
|
import 'widgets/password_field.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LoginPage extends StatefulWidget {
|
||||||
|
const LoginPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginPage> createState() => _LoginPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
late AnimationController _fadeController;
|
||||||
|
late AnimationController _slideController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_fadeController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1500),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation =
|
||||||
|
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
||||||
|
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
_emailController.text = 'test@example.com';
|
||||||
|
_passwordController.text = 'password';
|
||||||
|
_fadeController.forward();
|
||||||
|
_slideController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_fadeController.dispose();
|
||||||
|
_slideController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleLogin() async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulasi proses login
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: AppColor.primaryGradient,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: AppValue.padding),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _fadeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildLogo(),
|
||||||
|
SpaceHeight(48),
|
||||||
|
_buildLoginCard(),
|
||||||
|
SpaceHeight(24),
|
||||||
|
_buildFooter(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLogo() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Welcome Back',
|
||||||
|
style: AppStyle.h1.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SpaceHeight(8),
|
||||||
|
Text(
|
||||||
|
'Sign in to your account',
|
||||||
|
style: AppStyle.lg.copyWith(color: AppColor.textLight),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoginCard() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.white,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.black.withOpacity(0.1),
|
||||||
|
blurRadius: 30,
|
||||||
|
offset: const Offset(0, 15),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
LoginEmailField(controller: _emailController),
|
||||||
|
const SpaceHeight(24),
|
||||||
|
LoginPasswordField(controller: _passwordController),
|
||||||
|
const SpaceHeight(16),
|
||||||
|
_buildForgetPassword(),
|
||||||
|
const SpaceHeight(32),
|
||||||
|
_buildLoginButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildForgetPassword() {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Text(
|
||||||
|
'Forgot Password?',
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
color: AppColor.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoginButton() {
|
||||||
|
return AppElevatedButton(
|
||||||
|
text: 'Sign In',
|
||||||
|
isLoading: _isLoading,
|
||||||
|
onPressed: _isLoading ? null : _handleLogin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFooter() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Don't have an account? ",
|
||||||
|
style: AppStyle.md.copyWith(color: AppColor.textLight),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Text(
|
||||||
|
'Sign Up',
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
color: AppColor.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/presentation/pages/auth/login/widgets/email_field.dart
Normal file
21
lib/presentation/pages/auth/login/widgets/email_field.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
|
import '../../../../../common/validator/validator.dart';
|
||||||
|
import '../../../../components/field/field.dart';
|
||||||
|
|
||||||
|
class LoginEmailField extends StatelessWidget {
|
||||||
|
final TextEditingController? controller;
|
||||||
|
const LoginEmailField({super.key, this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppTextFormField(
|
||||||
|
title: 'Email',
|
||||||
|
hintText: 'Enter your email',
|
||||||
|
prefixIcon: LineIcons.envelope,
|
||||||
|
validator: AppValidator.validateEmail,
|
||||||
|
controller: controller,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
|
import '../../../../../common/validator/validator.dart';
|
||||||
|
import '../../../../components/field/field.dart';
|
||||||
|
|
||||||
|
class LoginPasswordField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
const LoginPasswordField({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppPasswordTextFormField(
|
||||||
|
title: 'Password',
|
||||||
|
prefixIcon: LineIcons.lock,
|
||||||
|
hintText: 'Enter your password',
|
||||||
|
validator: AppValidator.validatePassword,
|
||||||
|
controller: controller,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../../common/theme/theme.dart';
|
import '../../common/theme/theme.dart';
|
||||||
import '../components/assets/assets.gen.dart';
|
import '../components/assets/assets.gen.dart';
|
||||||
|
import '../router/app_router.gr.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SplashPage extends StatefulWidget {
|
class SplashPage extends StatefulWidget {
|
||||||
@ -79,7 +80,7 @@ class _SplashPageState extends State<SplashPage> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
void _navigateToHome() {
|
void _navigateToHome() {
|
||||||
// Uncomment dan sesuaikan dengan route yang ada
|
// Uncomment dan sesuaikan dengan route yang ada
|
||||||
// context.router.replace(const HomeRoute());
|
context.router.replace(const LoginRoute());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -7,5 +7,8 @@ class AppRouter extends RootStackRouter {
|
|||||||
List<AutoRoute> get routes => [
|
List<AutoRoute> get routes => [
|
||||||
// Splash
|
// Splash
|
||||||
AutoRoute(page: SplashRoute.page, initial: true),
|
AutoRoute(page: SplashRoute.page, initial: true),
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
AutoRoute(page: LoginRoute.page),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,22 +9,40 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
import 'package:apskel_owner_flutter/presentation/pages/splash_page.dart'
|
import 'package:apskel_owner_flutter/presentation/pages/auth/login/login_page.dart'
|
||||||
as _i1;
|
as _i1;
|
||||||
import 'package:auto_route/auto_route.dart' as _i2;
|
import 'package:apskel_owner_flutter/presentation/pages/splash_page.dart'
|
||||||
|
as _i2;
|
||||||
|
import 'package:auto_route/auto_route.dart' as _i3;
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i1.SplashPage]
|
/// [_i1.LoginPage]
|
||||||
class SplashRoute extends _i2.PageRouteInfo<void> {
|
class LoginRoute extends _i3.PageRouteInfo<void> {
|
||||||
const SplashRoute({List<_i2.PageRouteInfo>? children})
|
const LoginRoute({List<_i3.PageRouteInfo>? children})
|
||||||
|
: super(LoginRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'LoginRoute';
|
||||||
|
|
||||||
|
static _i3.PageInfo page = _i3.PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const _i1.LoginPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [_i2.SplashPage]
|
||||||
|
class SplashRoute extends _i3.PageRouteInfo<void> {
|
||||||
|
const SplashRoute({List<_i3.PageRouteInfo>? children})
|
||||||
: super(SplashRoute.name, initialChildren: children);
|
: super(SplashRoute.name, initialChildren: children);
|
||||||
|
|
||||||
static const String name = 'SplashRoute';
|
static const String name = 'SplashRoute';
|
||||||
|
|
||||||
static _i2.PageInfo page = _i2.PageInfo(
|
static _i3.PageInfo page = _i3.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i1.SplashPage();
|
return const _i2.SplashPage();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@ -366,6 +366,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_spinkit:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_spinkit
|
||||||
|
sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.2"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -560,6 +568,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
line_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: line_icons
|
||||||
|
sha256: "249d781d922f5437ac763d9c8f5a02cf5b499a6dc3f85e4b92e074cff0a932ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -28,6 +28,8 @@ dependencies:
|
|||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
awesome_dio_interceptor: ^1.3.0
|
awesome_dio_interceptor: ^1.3.0
|
||||||
|
line_icons: ^2.0.3
|
||||||
|
flutter_spinkit: ^5.2.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user