diff --git a/data/lib/repositories/auth_repository_impl.dart b/data/lib/repositories/auth_repository_impl.dart index d51dfb3..a9266e0 100644 --- a/data/lib/repositories/auth_repository_impl.dart +++ b/data/lib/repositories/auth_repository_impl.dart @@ -16,30 +16,30 @@ class AuthRepositoryImpl implements AuthRepository { AuthRepositoryImpl({required this.client, required this.authClient}); @override - Future signup(String username, String email, String password) async { + Future signup(String email, String password, {String? name}) async { final url = Uri.parse('http://localhost:3000/api/users/'); final response = await client.post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode( - {'username': username, 'email': email, 'password': password}), + {'email': email, 'password': password, + if (name != null) 'name': name,}), ); if (response.statusCode == 200 || response.statusCode == 201) { - return User(username: username, email: email); + return User(email: email, name: name,); } else { throw Exception('Failed to sign up. Status code: ${response.statusCode}'); } } @override - Future login(String email, String password, {String? nickname}) async { + Future login(String email, String password) async { final url = Uri.parse('http://localhost:3000/api/auth/login'); final response = await client.post( url, headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'email': email, 'password': password, - if (nickname != null) 'nickname': nickname,}), + body: jsonEncode({'email': email, 'password': password,}), ); if (response.statusCode == 200) { diff --git a/domain/lib/entities/user.dart b/domain/lib/entities/user.dart index 0aec7aa..9100657 100644 --- a/domain/lib/entities/user.dart +++ b/domain/lib/entities/user.dart @@ -1,12 +1,12 @@ class User { - final String username; + final String? username; final String email; String? name; final String? description; final String? avatarUrl; User( - {required this.username, + {this.username, required this.email, this.name, this.description, diff --git a/domain/lib/repositories_abstract/auth_repository.dart b/domain/lib/repositories_abstract/auth_repository.dart index 9c6d2c1..37eab9a 100644 --- a/domain/lib/repositories_abstract/auth_repository.dart +++ b/domain/lib/repositories_abstract/auth_repository.dart @@ -3,8 +3,8 @@ import '../entities/user.dart'; abstract class AuthRepository { - Future signup(String username, String email, String password); - Future login(String email, String password, {String? nickname}); + Future signup(String email, String password, {String? name}); + Future login(String email, String password); Future logout(); Future forgetPassword(String email); Future resetPassword(String email, String newPassword, String confirmationCode); diff --git a/domain/lib/usecases/auth_usecase.dart b/domain/lib/usecases/auth_usecase.dart index 13d8562..98be737 100644 --- a/domain/lib/usecases/auth_usecase.dart +++ b/domain/lib/usecases/auth_usecase.dart @@ -11,12 +11,12 @@ class AuthUseCase { AuthUseCase({required this.repository, required this.tokenProvider}); - Future signup(String username, String email, String password) async { - return await repository.signup(username, email, password); + Future signup(String email, String password, {String? name}) async { + return await repository.signup(email, password, name: name); } - Future login(String email, String password, {String? nickname}) async { - String accessToken = await repository.login(email, password, nickname: nickname); + Future login(String email, String password) async { + String accessToken = await repository.login(email, password); await tokenProvider.saveToken(accessToken); return accessToken; } diff --git a/lib/presentation/settings/viewModels/login_view_model.dart b/lib/presentation/settings/viewModels/login_view_model.dart index 13936ce..b4cadc9 100644 --- a/lib/presentation/settings/viewModels/login_view_model.dart +++ b/lib/presentation/settings/viewModels/login_view_model.dart @@ -30,13 +30,13 @@ class LoginViewModel extends ChangeNotifier { notifyListeners(); } - Future login(String email,String password, {String? nickname}) async { + Future login(String email,String password) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { - final accessToken = await authUseCase.login(email, password, nickname: nickname); + final accessToken = await authUseCase.login(email, password); return accessToken; // Successful login returns the access token } catch (e) { _errorMessage = 'Login failed: ${e.toString()}'; diff --git a/lib/presentation/settings/viewModels/signup_view_model.dart b/lib/presentation/settings/viewModels/signup_view_model.dart index 4c38d99..68cdc78 100644 --- a/lib/presentation/settings/viewModels/signup_view_model.dart +++ b/lib/presentation/settings/viewModels/signup_view_model.dart @@ -8,6 +8,20 @@ import 'package:domain/usecases/auth_usecase.dart'; class SignupViewModel extends ChangeNotifier { final AuthUseCase authUseCase; + bool obscureTextNewPassword = true; + bool obscureTextConfirmPassword = true; + + void toggleNewPasswordVisibility() { + obscureTextNewPassword = !obscureTextNewPassword; + notifyListeners(); // Notify the UI about the change + } + + void toggleConfirmPasswordVisibility() { + obscureTextConfirmPassword = !obscureTextConfirmPassword; + notifyListeners(); // Notify the UI about the change + } + + SignupViewModel({required this.authUseCase}); bool _isLoading = false; @@ -16,13 +30,13 @@ class SignupViewModel extends ChangeNotifier { String? _errorMessage; String? get errorMessage => _errorMessage; - Future signup(String username, String email, String password) async { + Future signup(String email, String password, {String? name}) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { - User user = await authUseCase.signup(username, email, password); + User user = await authUseCase.signup(email, password, name: name); return user; } catch (e) { _errorMessage = 'Signup failed: ${e.toString()}'; diff --git a/lib/presentation/settings/views/login_page.dart b/lib/presentation/settings/views/login_page.dart index beae6cc..0b82942 100644 --- a/lib/presentation/settings/views/login_page.dart +++ b/lib/presentation/settings/views/login_page.dart @@ -19,13 +19,12 @@ class _LoginPageState extends State { final _formKey = GlobalKey(); String email = ''; String password = ''; - String nickname = ''; bool isLoading = false; bool isButtonEnabled = false; bool isPasswordValid = false; final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _nicknameController = TextEditingController(); + @override @@ -51,9 +50,8 @@ class _LoginPageState extends State { try { // Call login from viewModel and pass the credentials - nickname = _nicknameController.text.trim(); - final accessToken = await viewModel.login(_emailController.text, _passwordController.text, - nickname: nickname.isNotEmpty ? nickname : null,); + + final accessToken = await viewModel.login(_emailController.text, _passwordController.text,); if (accessToken != null) { // Login successful @@ -148,20 +146,6 @@ class _LoginPageState extends State { ), SizedBox(height: 16.0), - TextFormField( - controller: _nicknameController, - decoration: InputDecoration( - labelText: 'Nickname (optional)', - hintText: 'Enter your nickname', // Optional hint text - border: UnderlineInputBorder(), - ), - style: TextStyle(color: Colors.black), // Text color when typing - obscureText: false, - onChanged: (value) => nickname = value.trim(), - ), - SizedBox(height: 16.0), - - // Password Field TextFormField( controller: _passwordController, diff --git a/lib/presentation/settings/views/sigup_page.dart b/lib/presentation/settings/views/sigup_page.dart index dadc5a0..cdd6463 100644 --- a/lib/presentation/settings/views/sigup_page.dart +++ b/lib/presentation/settings/views/sigup_page.dart @@ -32,20 +32,41 @@ class _SignupFormState extends State { String password = ''; String confirmPassword = ''; bool isLoading = false; + String nickname = ''; + final TextEditingController _nicknameController = TextEditingController(); + bool isButtonEnabled = false; + bool isPasswordValid = false; + + void _checkFields() { + setState(() { + isButtonEnabled = email.isNotEmpty && + RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(email) && + password.isNotEmpty && + password.length >= 6 && + confirmPassword == password; + }); + } void _signup(SignupViewModel viewModel) async { + nickname = _nicknameController.text.trim(); if (_formKey.currentState!.validate()) { - User? user = await viewModel.signup(username, email, password); + setState(() => isLoading = true); + User? user = await viewModel.signup( + email, + password, + name: nickname.isNotEmpty ? nickname : null, + ); + setState(() => isLoading = false); + if (user != null) { - // Show success message ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Signup successful!"), duration: Duration(seconds: 2),), + SnackBar( + content: Text("Signup successful!"), + duration: Duration(seconds: 2), + ), ); - - // Wait for a short delay before popping the screen Navigator.pop(context, user); } else if (viewModel.errorMessage != null) { - // Display an error message ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(viewModel.errorMessage!)), ); @@ -58,82 +79,146 @@ class _SignupFormState extends State { final viewModel = Provider.of(context); return Scaffold( - appBar: AppBar(title: Text('Signup')), - body: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( + appBar: AppBar(title: Text('Signup')), + body: SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + // Username Field + TextFormField( + decoration: InputDecoration( + labelText: 'Email', + errorBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFB71C1C)), + ), + focusedErrorBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFB71C1C)), + ), + errorStyle: TextStyle(color: Color(0xFFB71C1C)), + ), + onChanged: (value) { + email = value.trim(); + _checkFields(); + }, + validator: (value) { + if (value == null || value.isEmpty) return 'Please enter your email'; + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) return 'Enter a valid email'; + return null; + }, + ), + SizedBox(height: 16.0), + + // Nickname Field (Optional) + TextFormField( + controller: _nicknameController, + decoration: InputDecoration( + labelText: 'Nickname (optional)', + hintText: 'Enter your nickname', + ), + style: TextStyle(color: Colors.black), + onChanged: (value) { + nickname = value.trim(); + }, + ), + SizedBox(height: 16.0), + + // Password Field + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Username Field TextFormField( - decoration: InputDecoration(labelText: 'Username'), - onChanged: (value) => username = value.trim(), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username'; - } - return null; + decoration: InputDecoration( + labelText: 'Password', + suffixIcon: IconButton( + icon: Icon( + viewModel.obscureTextNewPassword + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: viewModel.toggleNewPasswordVisibility, + ), + ), + obscureText: viewModel.obscureTextNewPassword, + onChanged: (text) { + password = text; + setState(() { + isPasswordValid = password.length >= 6; + }); + _checkFields(); }, ), - SizedBox(height: 16.0), - // Email Field - TextFormField( - decoration: InputDecoration(labelText: 'Email'), - onChanged: (value) => email = value.trim(), - validator: (value) { - if (value == null || value.isEmpty) - return 'Please enter your email'; - if (!RegExp(r'^[^@]+@[^@]+\.[^@]+') - .hasMatch(value)) return 'Enter a valid email'; - return null; - }, + SizedBox(height: 4.0), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 3.0), + child: Text( + isPasswordValid ? '' : 'Password must be at least 6 characters long', + style: TextStyle( + color: isPasswordValid ? Colors.transparent : Colors.black54, + fontSize: 14.0, + ), + ), + ), ), - SizedBox(height: 16.0), - // Password Field - TextFormField( - decoration: InputDecoration(labelText: 'Password'), - obscureText: true, - onChanged: (value) => password = value.trim(), - validator: (value) { - if (value == null || value.isEmpty) - return 'Please enter your password'; - if (value.length < 6) - return 'Password must be at least 6 characters'; - return null; - }, + ], + ), + SizedBox(height: 16.0), + + // Confirm Password Field + TextFormField( + decoration: InputDecoration( + labelText: 'Confirm Password', + errorText: confirmPassword == password ? null : 'Passwords do not match', + errorBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFB71C1C)), ), - SizedBox(height: 16.0), - // Confirm Password Field - TextFormField( - decoration: InputDecoration(labelText: 'Confirm Password'), - obscureText: true, - onChanged: (value) => confirmPassword = value.trim(), - validator: (value) { - if (value == null || value.isEmpty) - return 'Please confirm your password'; - if (value != password) - return 'Passwords do not match'; - return null; - }, + focusedErrorBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFB71C1C)), ), - SizedBox(height: 30.0), - // Signup Button - // Signup Button - viewModel.isLoading - ? CircularProgressIndicator() - : MaterialButton( - onPressed: () => _signup(viewModel), - height: 45, - minWidth: double.infinity, - color: Color.fromRGBO(150, 150, 150, 1), // Match the grey color of "Register" button - disabledColor: Color.fromRGBO(150, 150, 150, 0.5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), // Rounded corners + errorStyle: TextStyle(color: Color(0xFFB71C1C)), + suffixIcon: IconButton( + icon: Icon( + viewModel.obscureTextConfirmPassword + ? Icons.visibility_off + : Icons.visibility, ), - child: Text('Signup', style: TextStyle(color: Colors.white, fontSize: 16),), - ), ], - )), - )); + onPressed: viewModel.toggleConfirmPasswordVisibility, + ), + ), + obscureText: viewModel.obscureTextConfirmPassword, + onChanged: (value) { + setState(() { + confirmPassword = value.trim(); + }); + _checkFields(); + }, + ), + SizedBox(height: 30.0), + + // Signup Button + viewModel.isLoading || isLoading + ? CircularProgressIndicator() + : MaterialButton( + onPressed: isButtonEnabled ? () => _signup(viewModel) : null, + height: 45, + minWidth: double.infinity, + color: isButtonEnabled + ? Color.fromRGBO(51, 66, 78, 1) + : Color.fromRGBO(180, 180, 180, 1), + disabledColor: Color.fromRGBO(140, 150, 153, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + child: Text('Signup', style: TextStyle(color: Colors.white, fontSize: 16)), + ), + ], + ), + ), + ), + ); } }