From f479acac91f23b7d812609cfa14912f3ec2a7fad Mon Sep 17 00:00:00 2001 From: akhdanre Date: Tue, 22 Apr 2025 15:08:02 +0700 Subject: [PATCH] feat: register feature --- lib/app/app.dart | 2 + lib/app/bindings/initial_bindings.dart | 9 ++ lib/app/routes/app_pages.dart | 9 +- lib/component/app_name.dart | 16 ++++ lib/core/endpoint/api_endpoint.dart | 2 + .../models/register/register_request.dart | 25 ++++++ .../models/register/register_response.dart | 0 lib/data/providers/dio_client.dart | 21 +++++ lib/data/services/auth_service.dart | 23 +++++ .../login/controllers/login_controller.dart | 32 +++---- .../component/google_button.dart | 0 .../component/register_text_button.dart | 4 +- .../{presentation => view}/login_page.dart | 18 ++-- .../register/binding/register_binding.dart | 11 +++ .../controller/register_controller.dart | 86 +++++++++++++++++++ lib/feature/register/view/register_page.dart | 70 +++++++++++++++ .../presentation/splash_screen_page.dart | 8 +- lib/global_controller.dart | 12 --- pubspec.lock | 16 ++++ pubspec.yaml | 2 +- 20 files changed, 316 insertions(+), 50 deletions(-) create mode 100644 lib/app/bindings/initial_bindings.dart create mode 100644 lib/component/app_name.dart create mode 100644 lib/data/models/register/register_request.dart create mode 100644 lib/data/models/register/register_response.dart create mode 100644 lib/data/providers/dio_client.dart create mode 100644 lib/data/services/auth_service.dart rename lib/feature/login/{presentation => view}/component/google_button.dart (100%) rename lib/feature/login/{presentation => view}/component/register_text_button.dart (88%) rename lib/feature/login/{presentation => view}/login_page.dart (72%) create mode 100644 lib/feature/register/binding/register_binding.dart create mode 100644 lib/feature/register/controller/register_controller.dart create mode 100644 lib/feature/register/view/register_page.dart delete mode 100644 lib/global_controller.dart diff --git a/lib/app/app.dart b/lib/app/app.dart index ca51c17..9bcc073 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get_navigation/src/root/get_material_app.dart'; +import 'package:quiz_app/app/bindings/initial_bindings.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; class MyApp extends StatelessWidget { @@ -10,6 +11,7 @@ class MyApp extends StatelessWidget { return GetMaterialApp( debugShowCheckedModeBanner: false, title: 'Quiz App', + initialBinding: InitialBindings(), initialRoute: AppRoutes.splashScreen, getPages: AppPages.routes, ); diff --git a/lib/app/bindings/initial_bindings.dart b/lib/app/bindings/initial_bindings.dart new file mode 100644 index 0000000..ea48a0b --- /dev/null +++ b/lib/app/bindings/initial_bindings.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:quiz_app/data/providers/dio_client.dart'; + +class InitialBindings extends Bindings { + @override + void dependencies() { + Get.putAsync(() => ApiClient().init()); + } +} diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index aaa8fd8..d43773c 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -2,7 +2,9 @@ import 'package:get/get_navigation/src/routes/get_route.dart'; import 'package:quiz_app/app/middleware/auth_middleware.dart'; import 'package:quiz_app/feature/home/presentation/home_page.dart'; import 'package:quiz_app/feature/login/bindings/login_binding.dart'; -import 'package:quiz_app/feature/login/presentation/login_page.dart'; +import 'package:quiz_app/feature/login/view/login_page.dart'; +import 'package:quiz_app/feature/register/binding/register_binding.dart'; +import 'package:quiz_app/feature/register/view/register_page.dart'; import 'package:quiz_app/feature/splash_screen/presentation/splash_screen_page.dart'; part 'app_routes.dart'; @@ -18,6 +20,11 @@ class AppPages { page: () => LoginView(), binding: LoginBinding(), ), + GetPage( + name: AppRoutes.registerPage, + page: () => RegisterView(), + binding: RegisterBinding(), + ), GetPage( name: AppRoutes.homePage, page: () => HomeView(), diff --git a/lib/component/app_name.dart b/lib/component/app_name.dart new file mode 100644 index 0000000..b0a2205 --- /dev/null +++ b/lib/component/app_name.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class AppName extends StatelessWidget { + const AppName({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text("GEN", style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold)), + Text("SO", style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.red)), + ], + ); + } +} diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart index e75ad03..7f975a4 100644 --- a/lib/core/endpoint/api_endpoint.dart +++ b/lib/core/endpoint/api_endpoint.dart @@ -3,4 +3,6 @@ class APIEndpoint { static const String login = "/login"; static const String loginGoogle = "/login/google"; + + static const String register = "/register"; } diff --git a/lib/data/models/register/register_request.dart b/lib/data/models/register/register_request.dart new file mode 100644 index 0000000..8059bcc --- /dev/null +++ b/lib/data/models/register/register_request.dart @@ -0,0 +1,25 @@ +class RegisterRequestModel { + final String email; + final String password; + final String name; + final String birthDate; + final String? phone; + + RegisterRequestModel({ + required this.email, + required this.password, + required this.name, + required this.birthDate, + this.phone, + }); + + Map toJson() { + return { + 'email': email, + 'password': password, + 'name': name, + 'birth_date': birthDate, + if (phone != null) 'phone': phone, + }; + } +} diff --git a/lib/data/models/register/register_response.dart b/lib/data/models/register/register_response.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/providers/dio_client.dart b/lib/data/providers/dio_client.dart new file mode 100644 index 0000000..c92b958 --- /dev/null +++ b/lib/data/providers/dio_client.dart @@ -0,0 +1,21 @@ +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:quiz_app/core/endpoint/api_endpoint.dart'; + +class ApiClient extends GetxService { + late final Dio dio; + + Future init() async { + dio = Dio(BaseOptions( + baseUrl: APIEndpoint.baseUrl, + connectTimeout: const Duration(minutes: 3), + receiveTimeout: const Duration(minutes: 10), + headers: { + "Content-Type": "application/json", + }, + )); + + dio.interceptors.add(LogInterceptor(responseBody: true)); + return this; + } +} diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart new file mode 100644 index 0000000..98163d6 --- /dev/null +++ b/lib/data/services/auth_service.dart @@ -0,0 +1,23 @@ +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:quiz_app/core/endpoint/api_endpoint.dart'; +import 'package:quiz_app/data/models/register/register_request.dart'; +import 'package:quiz_app/data/providers/dio_client.dart'; + +class AuthService extends GetxService { + late final Dio _dio; + + @override + void onInit() { + _dio = Get.find().dio; + super.onInit(); + } + + Future register(RegisterRequestModel request) async { + var data = await _dio.post( + APIEndpoint.register, + data: request.toJson(), + ); + print(data); + } +} diff --git a/lib/feature/login/controllers/login_controller.dart b/lib/feature/login/controllers/login_controller.dart index fb8cc20..b22e887 100644 --- a/lib/feature/login/controllers/login_controller.dart +++ b/lib/feature/login/controllers/login_controller.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter/material.dart'; +import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/core/endpoint/api_endpoint.dart'; import 'package:quiz_app/core/utils/logger.dart'; @@ -79,14 +80,13 @@ class LoginController extends GetxController { // Send ID Token to backend var response = await http.post( Uri.parse("${APIEndpoint.baseUrl}${APIEndpoint.loginGoogle}"), - body: jsonEncode({"token_id": idToken}), // Ensure correct key + body: jsonEncode({"token_id": idToken}), headers: {"Content-Type": "application/json"}, ); if (response.statusCode == 200) { var data = jsonDecode(response.body); - Get.snackbar("Success", "Google login successful!"); // logC.i("Backend Auth Token: $backendToken"); } else { @@ -100,19 +100,21 @@ class LoginController extends GetxController { } } - /// **🔹 Logout Function** - Future logout() async { - try { - await _googleSignIn.signOut(); - // await _secureStorage.delete(key: "auth_token"); + void goToRegsPage() => Get.toNamed(AppRoutes.registerPage); - emailController.clear(); - passwordController.clear(); + // /// **🔹 Logout Function** + // Future logout() async { + // try { + // await _googleSignIn.signOut(); + // // await _secureStorage.delete(key: "auth_token"); - Get.snackbar("Success", "Logged out successfully"); - } catch (e) { - logC.e("Logout error: $e"); - Get.snackbar("Error", "Logout failed"); - } - } + // emailController.clear(); + // passwordController.clear(); + + // Get.snackbar("Success", "Logged out successfully"); + // } catch (e) { + // logC.e("Logout error: $e"); + // Get.snackbar("Error", "Logout failed"); + // } + // } } diff --git a/lib/feature/login/presentation/component/google_button.dart b/lib/feature/login/view/component/google_button.dart similarity index 100% rename from lib/feature/login/presentation/component/google_button.dart rename to lib/feature/login/view/component/google_button.dart diff --git a/lib/feature/login/presentation/component/register_text_button.dart b/lib/feature/login/view/component/register_text_button.dart similarity index 88% rename from lib/feature/login/presentation/component/register_text_button.dart rename to lib/feature/login/view/component/register_text_button.dart index 4c33be2..3a36284 100644 --- a/lib/feature/login/presentation/component/register_text_button.dart +++ b/lib/feature/login/view/component/register_text_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; class RegisterTextButton extends StatelessWidget { - final VoidCallback? onTap; - const RegisterTextButton({super.key, this.onTap}); + final VoidCallback onTap; + const RegisterTextButton({super.key, required this.onTap}); @override Widget build(BuildContext context) { diff --git a/lib/feature/login/presentation/login_page.dart b/lib/feature/login/view/login_page.dart similarity index 72% rename from lib/feature/login/presentation/login_page.dart rename to lib/feature/login/view/login_page.dart index 7cc58c0..6718e6f 100644 --- a/lib/feature/login/presentation/login_page.dart +++ b/lib/feature/login/view/login_page.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:quiz_app/component/app_name.dart'; import 'package:quiz_app/component/global_button.dart'; import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/label_text_field.dart'; import 'package:quiz_app/feature/login/controllers/login_controller.dart'; -import 'package:quiz_app/feature/login/presentation/component/google_button.dart'; -import 'package:quiz_app/feature/login/presentation/component/register_text_button.dart'; +import 'package:quiz_app/feature/login/view/component/google_button.dart'; +import 'package:quiz_app/feature/login/view/component/register_text_button.dart'; class LoginView extends GetView { const LoginView({super.key}); @@ -18,16 +19,7 @@ class LoginView extends GetView { padding: const EdgeInsets.all(16.0), child: ListView( children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 40), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text("GEN", style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold)), - Text("SO", style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.red)), - ], - ), - ), + Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: AppName()), LabelTextField( label: "Log In", fontSize: 24, @@ -56,7 +48,7 @@ class LoginView extends GetView { onPress: controller.loginWithGoogle, ), const SizedBox(height: 20), - RegisterTextButton() + RegisterTextButton(onTap: controller.goToRegsPage) ], ), ), diff --git a/lib/feature/register/binding/register_binding.dart b/lib/feature/register/binding/register_binding.dart new file mode 100644 index 0000000..f00bd23 --- /dev/null +++ b/lib/feature/register/binding/register_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:quiz_app/data/services/auth_service.dart'; +import 'package:quiz_app/feature/register/controller/register_controller.dart'; + +class RegisterBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AuthService()); + Get.lazyPut(() => RegisterController(Get.find())); + } +} diff --git a/lib/feature/register/controller/register_controller.dart b/lib/feature/register/controller/register_controller.dart new file mode 100644 index 0000000..5428b9d --- /dev/null +++ b/lib/feature/register/controller/register_controller.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:quiz_app/data/models/register/register_request.dart'; +import 'package:quiz_app/data/services/auth_service.dart'; + +class RegisterController extends GetxController { + final AuthService _authService; + + RegisterController(this._authService); + + final TextEditingController nameController = TextEditingController(); + final TextEditingController bDateController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController confirmPasswordController = TextEditingController(); + final TextEditingController phoneController = TextEditingController(); + + var isPasswordHidden = true.obs; + var isConfirmPasswordHidden = true.obs; + + void togglePasswordVisibility() { + isPasswordHidden.value = !isPasswordHidden.value; + } + + void toggleConfirmPasswordVisibility() { + isConfirmPasswordHidden.value = !isConfirmPasswordHidden.value; + } + + Future onRegister() async { + String email = emailController.text.trim(); + String name = nameController.text.trim(); + String birthDate = bDateController.text.trim(); + String password = passwordController.text.trim(); + String confirmPassword = confirmPasswordController.text.trim(); + String phone = phoneController.text.trim(); + + if (email.isEmpty || password.isEmpty || confirmPassword.isEmpty || name.isEmpty || birthDate.isEmpty) { + Get.snackbar("Error", "All fields are required"); + return; + } + + if (!_isValidEmail(email)) { + Get.snackbar("Error", "Invalid email format"); + return; + } + + if (!_isValidDateFormat(birthDate)) { + Get.snackbar("Error", "Invalid date format. Use dd-mm-yyyy"); + return; + } + if (password != confirmPassword) { + Get.snackbar("Error", "Passwords do not match"); + return; + } + + if (phone.isNotEmpty && (phone.length < 10 || phone.length > 13)) { + Get.snackbar("Error", "Phone number must be between 10 and 13 digits"); + return; + } + + try { + await _authService.register( + RegisterRequestModel( + email: email, + password: password, + name: name, + birthDate: birthDate, + phone: phone, + ), + ); + Get.back(); + } catch (e) { + Get.snackbar("Error", "Failed to register: ${e.toString()}"); + } + } + + bool _isValidDateFormat(String date) { + final regex = RegExp(r'^([0-2][0-9]|(3)[0-1])\-((0[1-9])|(1[0-2]))\-\d{4}$'); + return regex.hasMatch(date); + } + + bool _isValidEmail(String email) { + final regex = RegExp(r"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"); + return regex.hasMatch(email); + } +} diff --git a/lib/feature/register/view/register_page.dart b/lib/feature/register/view/register_page.dart new file mode 100644 index 0000000..5e9621f --- /dev/null +++ b/lib/feature/register/view/register_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:quiz_app/component/app_name.dart'; +import 'package:quiz_app/component/global_button.dart'; +import 'package:quiz_app/component/global_text_field.dart'; +import 'package:quiz_app/component/label_text_field.dart'; +import 'package:quiz_app/feature/register/controller/register_controller.dart'; + +class RegisterView extends GetView { + const RegisterView({super.key}); + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 40), + child: AppName(), + ), + LabelTextField(label: "Register", fontSize: 24), + const SizedBox(height: 10), + LabelTextField(label: "Full Name"), + GlobalTextField(controller: controller.nameController), + const SizedBox(height: 10), + LabelTextField(label: "Email"), + GlobalTextField(controller: controller.emailController), + const SizedBox(height: 10), + LabelTextField(label: "Birth Date"), + GlobalTextField( + controller: controller.bDateController, + hintText: "12-08-2001", + ), + LabelTextField(label: "Nomer Telepon (Opsional)"), + GlobalTextField( + controller: controller.phoneController, + hintText: "085708570857", + ), + const SizedBox(height: 10), + LabelTextField(label: "Password"), + Obx( + () => GlobalTextField( + controller: controller.passwordController, + isPassword: true, + obscureText: controller.isPasswordHidden.value, + onToggleVisibility: controller.togglePasswordVisibility), + ), + const SizedBox(height: 10), + LabelTextField(label: "Verify Password"), + Obx( + () => GlobalTextField( + controller: controller.confirmPasswordController, + isPassword: true, + obscureText: controller.isConfirmPasswordHidden.value, + onToggleVisibility: controller.toggleConfirmPasswordVisibility), + ), + const SizedBox(height: 40), + GlobalButton( + onPressed: controller.onRegister, + text: "Register", + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/feature/splash_screen/presentation/splash_screen_page.dart b/lib/feature/splash_screen/presentation/splash_screen_page.dart index ad2c40e..5819aef 100644 --- a/lib/feature/splash_screen/presentation/splash_screen_page.dart +++ b/lib/feature/splash_screen/presentation/splash_screen_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; +import 'package:quiz_app/component/app_name.dart'; class SplashScreenView extends StatelessWidget { const SplashScreenView({super.key}); @@ -15,12 +16,7 @@ class SplashScreenView extends StatelessWidget { }); return Scaffold( - body: Center( - child: Text( - "Splash Screen", - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - ), + body: Center(child: AppName()), ); } } diff --git a/lib/global_controller.dart b/lib/global_controller.dart deleted file mode 100644 index 955214f..0000000 --- a/lib/global_controller.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:get/get.dart'; -import 'package:quiz_app/core/utils/logger.dart'; - -class GlobalController extends GetxController with WidgetsBindingObserver { - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - logC.i("state $state"); - } -} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 75761a7..b76549f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a723236..0b93ba6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,8 +37,8 @@ dependencies: logger: ^2.5.0 google_sign_in: ^6.2.2 - http: ^1.3.0 flutter_dotenv: ^5.2.1 + dio: ^5.8.0+1 dev_dependencies: flutter_test: