From 6298e52974b1c8619f966a45714811bc43b19734 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:02:48 +0700 Subject: [PATCH] coba run flutter nya --- lib/components/my_text_field.dart | 61 ++++ .../auth/blocs/sign_in_bloc/sign_in_bloc.dart | 25 ++ .../blocs/sign_in_bloc/sign_in_event.dart | 22 ++ .../blocs/sign_in_bloc/sign_in_state.dart | 14 + .../auth/blocs/sign_up_bloc/sign_up_bloc.dart | 23 ++ .../blocs/sign_up_bloc/sign_up_event.dart | 18 ++ .../blocs/sign_up_bloc/sign_up_state.dart | 14 + lib/screens/auth/views/sign_in_screen.dart | 142 +++++++++ lib/screens/auth/views/sign_up_screen.dart | 276 ++++++++++++++++++ lib/screens/auth/views/welcome_screen.dart | 123 +++++++- lib/screens/home/views/home_screen.dart | 2 +- 11 files changed, 717 insertions(+), 3 deletions(-) create mode 100644 lib/components/my_text_field.dart create mode 100644 lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart create mode 100644 lib/screens/auth/blocs/sign_in_bloc/sign_in_event.dart create mode 100644 lib/screens/auth/blocs/sign_in_bloc/sign_in_state.dart create mode 100644 lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart create mode 100644 lib/screens/auth/blocs/sign_up_bloc/sign_up_event.dart create mode 100644 lib/screens/auth/blocs/sign_up_bloc/sign_up_state.dart create mode 100644 lib/screens/auth/views/sign_in_screen.dart create mode 100644 lib/screens/auth/views/sign_up_screen.dart diff --git a/lib/components/my_text_field.dart b/lib/components/my_text_field.dart new file mode 100644 index 0000000..5b6370b --- /dev/null +++ b/lib/components/my_text_field.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class MyTextField extends StatelessWidget { + final TextEditingController controller; + final String hintText; + final bool obscureText; + final TextInputType keyboardType; + final Widget? suffixIcon; + final VoidCallback? onTap; + final Widget? prefixIcon; + final String? Function(String?)? validator; + final FocusNode? focusNode; + final String? errorMsg; + final String? Function(String?)? onChanged; + + const MyTextField({ + super.key, + required this.controller, + required this.hintText, + required this.obscureText, + required this.keyboardType, + this.suffixIcon, + this.onTap, + this.prefixIcon, + this.validator, + this.focusNode, + this.errorMsg, + this.onChanged + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + validator: validator, + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + focusNode: focusNode, + onTap: onTap, + textInputAction: TextInputAction.next, + onChanged: onChanged, + decoration: InputDecoration( + suffixIcon: suffixIcon, + prefixIcon: prefixIcon, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.transparent), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide(color: Theme.of(context).colorScheme.secondary), + ), + fillColor: Colors.grey.shade200, + filled: true, + hintText: hintText, + hintStyle: TextStyle(color: Colors.grey[500]), + errorText: errorMsg, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart new file mode 100644 index 0000000..1674865 --- /dev/null +++ b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:user_repository/user_repository.dart'; + +part 'sign_in_event.dart'; +part 'sign_in_state.dart'; + +class SignInBloc extends Bloc { + final UserRepository _userRepository; + + SignInBloc( + this._userRepository + ) : super(SignInInitial()) { + on((event, emit) async { + emit(SignInProcess()); + try { + await _userRepository.signIn(event.email, event.password); + } catch (e) { + emit(SignInFailure()); + } + }); + + on((event, emit) async => await _userRepository.logOut()); + } +} diff --git a/lib/screens/auth/blocs/sign_in_bloc/sign_in_event.dart b/lib/screens/auth/blocs/sign_in_bloc/sign_in_event.dart new file mode 100644 index 0000000..05203c4 --- /dev/null +++ b/lib/screens/auth/blocs/sign_in_bloc/sign_in_event.dart @@ -0,0 +1,22 @@ +part of 'sign_in_bloc.dart'; + +sealed class SignInEvent extends Equatable { + const SignInEvent(); + + @override + List get props => []; +} + +class SignInRequired extends SignInEvent { + final String email; + final String password; + + const SignInRequired(this.email, this.password); + + @override + List get props => [email, password]; +} + +class SignOutRequired extends SignInEvent { + +} \ No newline at end of file diff --git a/lib/screens/auth/blocs/sign_in_bloc/sign_in_state.dart b/lib/screens/auth/blocs/sign_in_bloc/sign_in_state.dart new file mode 100644 index 0000000..2783d42 --- /dev/null +++ b/lib/screens/auth/blocs/sign_in_bloc/sign_in_state.dart @@ -0,0 +1,14 @@ +part of 'sign_in_bloc.dart'; + +sealed class SignInState extends Equatable { + const SignInState(); + + @override + List get props => []; +} + +final class SignInInitial extends SignInState {} + +final class SignInFailure extends SignInState {} +final class SignInProcess extends SignInState {} +final class SignInSuccess extends SignInState {} diff --git a/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart b/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart new file mode 100644 index 0000000..0c1b996 --- /dev/null +++ b/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart @@ -0,0 +1,23 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:user_repository/user_repository.dart'; + +part 'sign_up_event.dart'; +part 'sign_up_state.dart'; + +class SignUpBloc extends Bloc { + final UserRepository _userRepository; + + SignUpBloc(this._userRepository) : super(SignUpInitial()) { + on((event, emit) async { + emit(SignUpProccess()); + try { + MyUser myUser = await _userRepository.signUp(event.user, event.password); + await _userRepository.setUserData(myUser); + emit(SignUpProccess()); + } catch (e) { + emit(SignUpFailure()); + } + }); + } +} diff --git a/lib/screens/auth/blocs/sign_up_bloc/sign_up_event.dart b/lib/screens/auth/blocs/sign_up_bloc/sign_up_event.dart new file mode 100644 index 0000000..e87ef90 --- /dev/null +++ b/lib/screens/auth/blocs/sign_up_bloc/sign_up_event.dart @@ -0,0 +1,18 @@ +part of 'sign_up_bloc.dart'; + +sealed class SignUpEvent extends Equatable { + const SignUpEvent(); + + @override + List get props => []; +} + +class SignUpRequired extends SignUpEvent { + final MyUser user; + final String password; + + const SignUpRequired(this.user, this.password); + + @override + List get props => [user, password]; +} \ No newline at end of file diff --git a/lib/screens/auth/blocs/sign_up_bloc/sign_up_state.dart b/lib/screens/auth/blocs/sign_up_bloc/sign_up_state.dart new file mode 100644 index 0000000..11a8f70 --- /dev/null +++ b/lib/screens/auth/blocs/sign_up_bloc/sign_up_state.dart @@ -0,0 +1,14 @@ +part of 'sign_up_bloc.dart'; + +sealed class SignUpState extends Equatable { + const SignUpState(); + + @override + List get props => []; +} + +final class SignUpInitial extends SignUpState {} + +final class SignUpSuccess extends SignUpState {} +final class SignUpFailure extends SignUpState {} +final class SignUpProccess extends SignUpState {} \ No newline at end of file diff --git a/lib/screens/auth/views/sign_in_screen.dart b/lib/screens/auth/views/sign_in_screen.dart new file mode 100644 index 0000000..3e7de9e --- /dev/null +++ b/lib/screens/auth/views/sign_in_screen.dart @@ -0,0 +1,142 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../components/my_text_field.dart'; +import '../blocs/sign_in_bloc/sign_in_bloc.dart'; +// import '../blocs/sing_in_bloc/sign_in_bloc.dart'; + +class SignInScreen extends StatefulWidget { + const SignInScreen({super.key}); + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + final passwordController = TextEditingController(); + final emailController = TextEditingController(); + final _formKey = GlobalKey(); + bool signInRequired = false; + IconData iconPassword = CupertinoIcons.eye_fill; + bool obscurePassword = true; + String? _errorMsg; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if(state is SignInSuccess) { + setState(() { + signInRequired = false; + }); + } else if(state is SignInProcess) { + setState(() { + signInRequired = true; + }); + } else if(state is SignInFailure) { + setState(() { + signInRequired = false; + _errorMsg = 'Invalid email or password'; + }); + } + }, + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 20), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: MyTextField( + controller: emailController, + hintText: 'Email', + obscureText: false, + keyboardType: TextInputType.emailAddress, + prefixIcon: const Icon(CupertinoIcons.mail_solid), + errorMsg: _errorMsg, + validator: (val) { + if (val!.isEmpty) { + return 'Please fill in this field'; + } else if (!RegExp(r'^[\w-\.]+@([\w-]+.)+[\w-]{2,4}$').hasMatch(val)) { + return 'Please enter a valid email'; + } + return null; + } + ) + ), + const SizedBox(height: 10), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: MyTextField( + controller: passwordController, + hintText: 'Password', + obscureText: obscurePassword, + keyboardType: TextInputType.visiblePassword, + prefixIcon: const Icon(CupertinoIcons.lock_fill), + errorMsg: _errorMsg, + validator: (val) { + if (val!.isEmpty) { + return 'Please fill in this field'; + } else if (!RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~`)\%\-(_+=;:,.<>/?"[{\]}\|^]).{8,}$').hasMatch(val)) { + return 'Please enter a valid password'; + } + return null; + }, + suffixIcon: IconButton( + onPressed: () { + setState(() { + obscurePassword = !obscurePassword; + if(obscurePassword) { + iconPassword = CupertinoIcons.eye_fill; + } else { + iconPassword = CupertinoIcons.eye_slash_fill; + } + }); + }, + icon: Icon(iconPassword), + ), + ), + ), + const SizedBox(height: 20), + !signInRequired + ? SizedBox( + width: MediaQuery.of(context).size.width * 0.5, + child: TextButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + context.read().add(SignInRequired( + emailController.text, + passwordController.text) + ); + } + }, + style: TextButton.styleFrom( + elevation: 3.0, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(60) + ) + ), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 25, vertical: 5), + child: Text( + 'Sign In', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600 + ), + ), + ) + ), + ) + : const CircularProgressIndicator(), + ], + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/auth/views/sign_up_screen.dart b/lib/screens/auth/views/sign_up_screen.dart new file mode 100644 index 0000000..0784298 --- /dev/null +++ b/lib/screens/auth/views/sign_up_screen.dart @@ -0,0 +1,276 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:user_repository/user_repository.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../components/my_text_field.dart'; +import '../blocs/sign_up_bloc/sign_up_bloc.dart'; + +class SignUpScreen extends StatefulWidget { + const SignUpScreen({super.key}); + + @override + State createState() => _SignUpScreenState(); +} + +class _SignUpScreenState extends State { + final passwordController = TextEditingController(); + final emailController = TextEditingController(); + final nameController = TextEditingController(); + final _formKey = GlobalKey(); + IconData iconPassword = CupertinoIcons.eye_fill; + bool obscurePassword = true; + bool signUpRequired = false; + + bool containsUpperCase = false; + bool containsLowerCase = false; + bool containsNumber = false; + bool containsSpecialChar = false; + bool contains8Length = false; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if(state is SignUpSuccess) { + setState(() { + signUpRequired = false; + }); + } else if(state is SignUpProccess) { + setState(() { + signUpRequired = true; + }); + } else if(state is SignUpFailure) { + return; + } + }, + child: Form( + key: _formKey, + child: Center( + child: Column( + children: [ + const SizedBox(height: 20), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: MyTextField( + controller: emailController, + hintText: 'Email', + obscureText: false, + keyboardType: TextInputType.emailAddress, + prefixIcon: const Icon(CupertinoIcons.mail_solid), + validator: (val) { + if(val!.isEmpty) { + return 'Please fill in this field'; + } else if(!RegExp(r'^[\w-\.]+@([\w-]+.)+[\w-]{2,4}$').hasMatch(val)) { + return 'Please enter a valid email'; + } + return null; + } + ), + ), + const SizedBox(height: 10), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: MyTextField( + controller: passwordController, + hintText: 'Password', + obscureText: obscurePassword, + keyboardType: TextInputType.visiblePassword, + prefixIcon: const Icon(CupertinoIcons.lock_fill), + onChanged: (val) { + if(val!.contains(RegExp(r'[A-Z]'))) { + setState(() { + containsUpperCase = true; + }); + } else { + setState(() { + containsUpperCase = false; + }); + } + if(val.contains(RegExp(r'[a-z]'))) { + setState(() { + containsLowerCase = true; + }); + } else { + setState(() { + containsLowerCase = false; + }); + } + if(val.contains(RegExp(r'[0-9]'))) { + setState(() { + containsNumber = true; + }); + } else { + setState(() { + containsNumber = false; + }); + } + if(val.contains(RegExp(r'^(?=.*?[!@#$&*~`)\%\-(_+=;:,.<>/?"[{\]}\|^])'))) { + setState(() { + containsSpecialChar = true; + }); + } else { + setState(() { + containsSpecialChar = false; + }); + } + if(val.length >= 8) { + setState(() { + contains8Length = true; + }); + } else { + setState(() { + contains8Length = false; + }); + } + return null; + }, + suffixIcon: IconButton( + onPressed: () { + setState(() { + obscurePassword = !obscurePassword; + if(obscurePassword) { + iconPassword = CupertinoIcons.eye_fill; + } else { + iconPassword = CupertinoIcons.eye_slash_fill; + } + }); + }, + icon: Icon(iconPassword), + ), + validator: (val) { + if(val!.isEmpty) { + return 'Please fill in this field'; + } else if(!RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~`)\%\-(_+=;:,.<>/?"[{\]}\|^]).{8,}$').hasMatch(val)) { + return 'Please enter a valid password'; + } + return null; + } + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "⚈ 1 uppercase", + style: TextStyle( + color: containsUpperCase + ? Colors.green + : Theme.of(context).colorScheme.onBackground + ), + ), + Text( + "⚈ 1 lowercase", + style: TextStyle( + color: containsLowerCase + ? Colors.green + : Theme.of(context).colorScheme.onBackground + ), + ), + Text( + "⚈ 1 number", + style: TextStyle( + color: containsNumber + ? Colors.green + : Theme.of(context).colorScheme.onBackground + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "⚈ 1 special character", + style: TextStyle( + color: containsSpecialChar + ? Colors.green + : Theme.of(context).colorScheme.onBackground + ), + ), + Text( + "⚈ 8 minimum character", + style: TextStyle( + color: contains8Length + ? Colors.green + : Theme.of(context).colorScheme.onBackground + ), + ), + ], + ), + ], + ), + const SizedBox(height: 10), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: MyTextField( + controller: nameController, + hintText: 'Name', + obscureText: false, + keyboardType: TextInputType.name, + prefixIcon: const Icon(CupertinoIcons.person_fill), + validator: (val) { + if(val!.isEmpty) { + return 'Please fill in this field'; + } else if(val.length > 30) { + return 'Name too long'; + } + return null; + } + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.02), + !signUpRequired + ? SizedBox( + width: MediaQuery.of(context).size.width * 0.5, + child: TextButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + MyUser myUser = MyUser.empty; + myUser.email = emailController.text; + myUser.name = nameController.text; + + setState(() { + context.read().add( + SignUpRequired( + myUser, + passwordController.text + ) + ); + }); + } + }, + style: TextButton.styleFrom( + elevation: 3.0, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(60) + ) + ), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 25, vertical: 5), + child: Text( + 'Sign Up', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600 + ), + ), + ) + ), + ) + : const CircularProgressIndicator() + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/auth/views/welcome_screen.dart b/lib/screens/auth/views/welcome_screen.dart index 2f23918..79ea53c 100644 --- a/lib/screens/auth/views/welcome_screen.dart +++ b/lib/screens/auth/views/welcome_screen.dart @@ -1,10 +1,129 @@ +import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../blocs/authentication_bloc/authentication_bloc.dart'; +import '../blocs/sign_in_bloc/sign_in_bloc.dart'; +import '../blocs/sign_up_bloc/sign_up_bloc.dart'; +import 'sign_in_screen.dart'; +import 'sign_up_screen.dart'; -class WelcomeScreen extends StatelessWidget { +class WelcomeScreen extends StatefulWidget { const WelcomeScreen({super.key}); + @override + State createState() => _WelcomeScreenState(); +} + +class _WelcomeScreenState extends State with TickerProviderStateMixin { + late TabController tabController; + + @override + void initState() { + tabController = TabController( + initialIndex: 0, + length: 2, + vsync: this + ); + super.initState(); + } + + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: SingleChildScrollView( + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: Stack( + children: [ + Align( + alignment: const AlignmentDirectional(20, -1.2), + child: Container( + height: MediaQuery.of(context).size.width, + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.tertiary + ), + ), + ), + Align( + alignment: const AlignmentDirectional(2.7, -1.2), + child: Container( + height: MediaQuery.of(context).size.width / 1.3, + width: MediaQuery.of(context).size.width / 1.3, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary + ), + ), + ), + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 100.0, sigmaY: 100.0), + child: Container(), + ), + Align( + alignment: Alignment.center, + child: SizedBox( + height: MediaQuery.of(context).size.height / 1.8, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 50.0), + child: TabBar( + controller: tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), + labelColor: Theme.of(context).colorScheme.onBackground, + tabs: const [ + Padding( + padding: EdgeInsets.all(12.0), + child: Text( + 'Sign In', + style: TextStyle( + fontSize: 18, + ), + ), + ), + Padding( + padding: EdgeInsets.all(12.0), + child: Text( + 'Sign Up', + style: TextStyle( + fontSize: 18, + ), + ), + ), + ], + ), + ), + Expanded( + child: TabBarView( + controller: tabController, + children: [ + BlocProvider( + create: (context) => SignInBloc( + context.read().userRepository + ), + child: const SignInScreen(), + ), + BlocProvider( + create: (context) => SignUpBloc( + context.read().userRepository + ), + child: const SignUpScreen(), + ), + ], + ) + ) + ], + ), + ), + ) + ], + ), + ), + ), + ); } } \ No newline at end of file diff --git a/lib/screens/home/views/home_screen.dart b/lib/screens/home/views/home_screen.dart index 352405d..0ebe19b 100644 --- a/lib/screens/home/views/home_screen.dart +++ b/lib/screens/home/views/home_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; class HomeScreen extends StatelessWidget{ const HomeScreen ({super.key}); - + @override Widget build(BuildContext context) { return Scaffold(