feat: register feature

This commit is contained in:
akhdanre 2025-04-22 15:08:02 +07:00
parent 32404aceae
commit f479acac91
20 changed files with 316 additions and 50 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get_navigation/src/root/get_material_app.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'; import 'package:quiz_app/app/routes/app_pages.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -10,6 +11,7 @@ class MyApp extends StatelessWidget {
return GetMaterialApp( return GetMaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Quiz App', title: 'Quiz App',
initialBinding: InitialBindings(),
initialRoute: AppRoutes.splashScreen, initialRoute: AppRoutes.splashScreen,
getPages: AppPages.routes, getPages: AppPages.routes,
); );

View File

@ -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());
}
}

View File

@ -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/app/middleware/auth_middleware.dart';
import 'package:quiz_app/feature/home/presentation/home_page.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/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'; import 'package:quiz_app/feature/splash_screen/presentation/splash_screen_page.dart';
part 'app_routes.dart'; part 'app_routes.dart';
@ -18,6 +20,11 @@ class AppPages {
page: () => LoginView(), page: () => LoginView(),
binding: LoginBinding(), binding: LoginBinding(),
), ),
GetPage(
name: AppRoutes.registerPage,
page: () => RegisterView(),
binding: RegisterBinding(),
),
GetPage( GetPage(
name: AppRoutes.homePage, name: AppRoutes.homePage,
page: () => HomeView(), page: () => HomeView(),

View File

@ -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)),
],
);
}
}

View File

@ -3,4 +3,6 @@ class APIEndpoint {
static const String login = "/login"; static const String login = "/login";
static const String loginGoogle = "/login/google"; static const String loginGoogle = "/login/google";
static const String register = "/register";
} }

View File

@ -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<String, dynamic> toJson() {
return {
'email': email,
'password': password,
'name': name,
'birth_date': birthDate,
if (phone != null) 'phone': phone,
};
}
}

View File

@ -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<ApiClient> 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;
}
}

View File

@ -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<ApiClient>().dio;
super.onInit();
}
Future<void> register(RegisterRequestModel request) async {
var data = await _dio.post(
APIEndpoint.register,
data: request.toJson(),
);
print(data);
}
}

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:flutter/material.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/endpoint/api_endpoint.dart';
import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/core/utils/logger.dart';
@ -79,14 +80,13 @@ class LoginController extends GetxController {
// Send ID Token to backend // Send ID Token to backend
var response = await http.post( var response = await http.post(
Uri.parse("${APIEndpoint.baseUrl}${APIEndpoint.loginGoogle}"), Uri.parse("${APIEndpoint.baseUrl}${APIEndpoint.loginGoogle}"),
body: jsonEncode({"token_id": idToken}), // Ensure correct key body: jsonEncode({"token_id": idToken}),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = jsonDecode(response.body); var data = jsonDecode(response.body);
Get.snackbar("Success", "Google login successful!"); Get.snackbar("Success", "Google login successful!");
// logC.i("Backend Auth Token: $backendToken"); // logC.i("Backend Auth Token: $backendToken");
} else { } else {
@ -100,19 +100,21 @@ class LoginController extends GetxController {
} }
} }
/// **🔹 Logout Function** void goToRegsPage() => Get.toNamed(AppRoutes.registerPage);
Future<void> logout() async {
try {
await _googleSignIn.signOut();
// await _secureStorage.delete(key: "auth_token");
emailController.clear(); // /// **🔹 Logout Function**
passwordController.clear(); // Future<void> logout() async {
// try {
// await _googleSignIn.signOut();
// // await _secureStorage.delete(key: "auth_token");
Get.snackbar("Success", "Logged out successfully"); // emailController.clear();
} catch (e) { // passwordController.clear();
logC.e("Logout error: $e");
Get.snackbar("Error", "Logout failed"); // Get.snackbar("Success", "Logged out successfully");
} // } catch (e) {
} // logC.e("Logout error: $e");
// Get.snackbar("Error", "Logout failed");
// }
// }
} }

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class RegisterTextButton extends StatelessWidget { class RegisterTextButton extends StatelessWidget {
final VoidCallback? onTap; final VoidCallback onTap;
const RegisterTextButton({super.key, this.onTap}); const RegisterTextButton({super.key, required this.onTap});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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_button.dart';
import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/global_text_field.dart';
import 'package:quiz_app/component/label_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/controllers/login_controller.dart';
import 'package:quiz_app/feature/login/presentation/component/google_button.dart'; import 'package:quiz_app/feature/login/view/component/google_button.dart';
import 'package:quiz_app/feature/login/presentation/component/register_text_button.dart'; import 'package:quiz_app/feature/login/view/component/register_text_button.dart';
class LoginView extends GetView<LoginController> { class LoginView extends GetView<LoginController> {
const LoginView({super.key}); const LoginView({super.key});
@ -18,16 +19,7 @@ class LoginView extends GetView<LoginController> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: ListView( child: ListView(
children: [ children: [
Padding( Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: AppName()),
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)),
],
),
),
LabelTextField( LabelTextField(
label: "Log In", label: "Log In",
fontSize: 24, fontSize: 24,
@ -56,7 +48,7 @@ class LoginView extends GetView<LoginController> {
onPress: controller.loginWithGoogle, onPress: controller.loginWithGoogle,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
RegisterTextButton() RegisterTextButton(onTap: controller.goToRegsPage)
], ],
), ),
), ),

View File

@ -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<AuthService>()));
}
}

View File

@ -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<void> 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);
}
}

View File

@ -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<RegisterController> {
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",
)
],
),
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/component/app_name.dart';
class SplashScreenView extends StatelessWidget { class SplashScreenView extends StatelessWidget {
const SplashScreenView({super.key}); const SplashScreenView({super.key});
@ -15,12 +16,7 @@ class SplashScreenView extends StatelessWidget {
}); });
return Scaffold( return Scaffold(
body: Center( body: Center(child: AppName()),
child: Text(
"Splash Screen",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
); );
} }
} }

View File

@ -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");
}
}

View File

@ -49,6 +49,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,8 +37,8 @@ dependencies:
logger: ^2.5.0 logger: ^2.5.0
google_sign_in: ^6.2.2 google_sign_in: ^6.2.2
http: ^1.3.0
flutter_dotenv: ^5.2.1 flutter_dotenv: ^5.2.1
dio: ^5.8.0+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: