feat: login api

This commit is contained in:
efrilm
2025-08-16 17:27:57 +07:00
parent 6019036cd7
commit 255b10d5a9
31 changed files with 2997 additions and 86 deletions
@@ -1,30 +1,32 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:math' as math;
import '../../../../application/auth/login_form/login_form_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../injection.dart';
import '../../../components/button/button.dart';
import '../../../components/spacer/spacer.dart';
import '../../../components/toast/flushbar.dart';
import '../../../router/app_router.gr.dart';
import 'widgets/email_field.dart';
import 'widgets/password_field.dart';
@RoutePage()
class LoginPage extends StatefulWidget {
class LoginPage extends StatefulWidget implements AutoRouteWrapper {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
@override
Widget wrappedRoute(BuildContext context) =>
BlocProvider(create: (_) => getIt<LoginFormBloc>(), child: this);
}
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 AnimationController _backgroundController;
@@ -77,9 +79,6 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
CurvedAnimation(parent: _floatingController, curve: Curves.easeInOut),
);
_emailController.text = 'test@example.com';
_passwordController.text = 'password';
_fadeController.forward();
_slideController.forward();
}
@@ -90,78 +89,88 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
_slideController.dispose();
_backgroundController.dispose();
_floatingController.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;
});
context.router.replace(const MainRoute());
}
context.read<LoginFormBloc>().add(LoginFormEvent.submitted());
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: Listenable.merge([
_backgroundController,
_floatingController,
]),
builder: (context, child) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: AppColor.primaryGradient,
return BlocListener<LoginFormBloc, LoginFormState>(
listenWhen: (previous, current) =>
previous.failureOrAuthOption != current.failureOrAuthOption,
listener: (context, state) {
state.failureOrAuthOption.fold(
() => null,
(either) => either.fold(
(f) => AppFlushbar.showAuthFailureToast(context, f),
(user) {
if (context.mounted) {
context.router.replace(const MainRoute());
}
},
),
);
},
child: Scaffold(
body: AnimatedBuilder(
animation: Listenable.merge([
_backgroundController,
_floatingController,
]),
builder: (context, child) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: AppColor.primaryGradient,
),
),
),
child: Stack(
children: [
// Animated background elements
_buildAnimatedBackground(),
child: Stack(
children: [
// Animated background elements
_buildAnimatedBackground(),
// Main content
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(context),
SpaceHeight(48),
_buildLoginCard(context),
],
// Main content
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: AppValue.padding,
),
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: BlocBuilder<LoginFormBloc, LoginFormState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLogo(context),
SpaceHeight(48),
_buildLoginCard(
context,
state.isSubmitting,
state.showErrorMessages,
),
],
);
},
),
),
),
),
),
),
),
],
),
);
},
],
),
);
},
),
),
);
}
@@ -312,7 +321,11 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
);
}
Widget _buildLoginCard(BuildContext context) {
Widget _buildLoginCard(
BuildContext context,
bool isLoading,
bool showErrorMessages,
) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
@@ -335,17 +348,20 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
],
),
child: Form(
key: _formKey,
autovalidateMode: showErrorMessages
? AutovalidateMode.always
: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LoginEmailField(controller: _emailController),
LoginEmailField(),
const SpaceHeight(24),
LoginPasswordField(controller: _passwordController),
LoginPasswordField(),
const SpaceHeight(16),
_buildForgetPassword(context),
const SpaceHeight(32),
_buildLoginButton(),
_buildLoginButton(isLoading),
],
),
),
@@ -368,11 +384,11 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
);
}
Widget _buildLoginButton() {
Widget _buildLoginButton(bool isLoading) {
return AppElevatedButton(
text: context.lang.sign_in,
isLoading: _isLoading,
onPressed: _isLoading ? null : _handleLogin,
isLoading: isLoading,
onPressed: _handleLogin,
);
}
}
@@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart';
import '../../../../../application/auth/login_form/login_form_bloc.dart';
import '../../../../../common/extension/extension.dart';
import '../../../../../common/validator/validator.dart';
import '../../../../components/field/field.dart';
class LoginEmailField extends StatelessWidget {
final TextEditingController? controller;
const LoginEmailField({super.key, this.controller});
const LoginEmailField({super.key});
@override
Widget build(BuildContext context) {
@@ -15,8 +16,10 @@ class LoginEmailField extends StatelessWidget {
title: context.lang.email,
hintText: context.lang.email_placeholder,
prefixIcon: LineIcons.envelope,
validator: AppValidator.validateEmail,
controller: controller,
validator: (value) =>
AppValidator.validateEmail(context.read<LoginFormBloc>().state.email),
onChanged: (value) =>
context.read<LoginFormBloc>().add(LoginFormEvent.emailChanged(value)),
);
}
}
@@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart';
import '../../../../../application/auth/login_form/login_form_bloc.dart';
import '../../../../../common/extension/extension.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});
const LoginPasswordField({super.key});
@override
Widget build(BuildContext context) {
@@ -15,8 +16,12 @@ class LoginPasswordField extends StatelessWidget {
title: context.lang.password,
prefixIcon: LineIcons.lock,
hintText: context.lang.password_placeholder,
validator: AppValidator.validatePassword,
controller: controller,
validator: (value) => AppValidator.validatePassword(
context.read<LoginFormBloc>().state.password,
),
onChanged: (value) => context.read<LoginFormBloc>().add(
LoginFormEvent.passwordChanged(value),
),
);
}
}