fix: adding back limitation

This commit is contained in:
akhdanre 2025-06-12 04:11:25 +07:00
parent 576f5b27ea
commit 0a138793ae
15 changed files with 421 additions and 336 deletions

View File

@ -14,7 +14,9 @@ class CustomFloatingLoading {
color: Colors.black.withValues(alpha: 0.5), color: Colors.black.withValues(alpha: 0.5),
), ),
const Center( const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(
color: Colors.white,
),
), ),
], ],
), ),

View File

@ -43,15 +43,17 @@ class DetailHistoryView extends GetView<DetailHistoryController> {
List<Widget> quizListings() { List<Widget> quizListings() {
return controller.quizAnswer.questionListings return controller.quizAnswer.questionListings
.map((e) => QuizItemWAComponent( .asMap()
index: e.index, .entries
isCorrect: e.isCorrect, .map((entry) => QuizItemWAComponent(
question: e.question, index: entry.key + 1,
targetAnswer: e.targetAnswer, isCorrect: entry.value.isCorrect,
timeSpent: e.timeSpent, question: entry.value.question,
type: e.type, targetAnswer: entry.value.targetAnswer,
userAnswer: e.userAnswer, timeSpent: entry.value.timeSpent,
options: e.options, type: entry.value.type,
userAnswer: entry.value.userAnswer,
options: entry.value.options,
)) ))
.toList(); .toList();
} }

View File

@ -24,6 +24,7 @@ class JoinRoomController extends GetxController {
); );
final TextEditingController codeController = TextEditingController(); final TextEditingController codeController = TextEditingController();
RxBool isLoading = false.obs;
void joinRoom(BuildContext context) { void joinRoom(BuildContext context) {
if (!_connectionService.isCurrentlyConnected) { if (!_connectionService.isCurrentlyConnected) {
@ -42,12 +43,14 @@ class JoinRoomController extends GetxController {
return; return;
} }
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoading(Get.overlayContext!);
isLoading.value = true;
_socketService.initSocketConnection(); _socketService.initSocketConnection();
_socketService.joinRoom(sessionCode: code, userId: _userController.userData!.id); _socketService.joinRoom(sessionCode: code, userId: _userController.userData!.id);
_socketService.errors.listen((error) { _socketService.errors.listen((error) {
CustomNotification.error(title: "not found", message: "Ruangan tidak ditemukan"); CustomNotification.error(title: "not found", message: "Ruangan tidak ditemukan");
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
}); });
_socketService.roomMessages.listen((data) { _socketService.roomMessages.listen((data) {
@ -57,6 +60,7 @@ class JoinRoomController extends GetxController {
final Map<String, dynamic> quizInfoJson = dataPayload["quiz_info"]; final Map<String, dynamic> quizInfoJson = dataPayload["quiz_info"];
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
Get.toNamed( Get.toNamed(
AppRoutes.waitRoomPage, AppRoutes.waitRoomPage,
arguments: WaitingRoomDTO( arguments: WaitingRoomDTO(
@ -73,6 +77,10 @@ class JoinRoomController extends GetxController {
}); });
} }
void onGoBack() {
if (!isLoading.value) Get.back();
}
@override @override
void onClose() { void onClose() {
codeController.dispose(); codeController.dispose();

View File

@ -12,185 +12,189 @@ class JoinRoomView extends GetView<JoinRoomController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
backgroundColor: Colors.white, canPop: false,
extendBodyBehindAppBar: true, onPopInvokedWithResult: (didPop, result) => controller.onGoBack(),
appBar: AppBar( child: Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.white,
elevation: 0, extendBodyBehindAppBar: true,
leading: IconButton( appBar: AppBar(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.black87), backgroundColor: Colors.transparent,
onPressed: () => Get.back(), elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.black87),
onPressed: () => Get.back(),
),
), ),
), body: Container(
body: Container( color: Colors.white,
color: Colors.white, child: SafeArea(
child: SafeArea( child: SingleChildScrollView(
child: SingleChildScrollView( padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ const SizedBox(height: 20),
const SizedBox(height: 20),
// TweenAnimationBuilder<double>( // TweenAnimationBuilder<double>(
// duration: const Duration(seconds: 1), // duration: const Duration(seconds: 1),
// tween: Tween(begin: 0.0, end: 1.0), // tween: Tween(begin: 0.0, end: 1.0),
// builder: (context, value, child) { // builder: (context, value, child) {
// return Transform.scale( // return Transform.scale(
// scale: value, // scale: value,
// child: child, // child: child,
// ); // );
// }, // },
// child: Container( // child: Container(
// padding: EdgeInsets.all(22), // padding: EdgeInsets.all(22),
// decoration: BoxDecoration( // decoration: BoxDecoration(
// color: AppColors.primaryBlue.withValues(alpha: 0.05), // color: AppColors.primaryBlue.withValues(alpha: 0.05),
// shape: BoxShape.circle, // shape: BoxShape.circle,
// border: Border.all( // border: Border.all(
// color: AppColors.primaryBlue.withValues(alpha: 0.15), // color: AppColors.primaryBlue.withValues(alpha: 0.15),
// width: 2, // width: 2,
// ), // ),
// ), // ),
// child: Icon( // child: Icon(
// LucideIcons.trophy, // LucideIcons.trophy,
// size: 70, // size: 70,
// color: AppColors.primaryBlue, // color: AppColors.primaryBlue,
// ), // ),
// ), // ),
// ), // ),
const SizedBox(height: 30), const SizedBox(height: 30),
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 800), duration: const Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) { builder: (context, value, child) {
return Opacity( return Opacity(
opacity: value, opacity: value,
child: Transform.translate( child: Transform.translate(
offset: Offset(0, 20 * (1 - value)), offset: Offset(0, 20 * (1 - value)),
child: child, child: child,
), ),
); );
}, },
child: Text(
context.tr("ready_to_compete"),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 15),
// Animated Subtitle
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text( child: Text(
context.tr("enter_code_to_join"), context.tr("ready_to_compete"),
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 28,
color: Colors.black54, fontWeight: FontWeight.bold,
height: 1.4, color: Colors.black87,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
),
const SizedBox(height: 40), const SizedBox(height: 15),
TweenAnimationBuilder<double>( // Animated Subtitle
duration: const Duration(milliseconds: 1000), TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 800),
builder: (context, value, child) { tween: Tween(begin: 0.0, end: 1.0),
return Opacity( builder: (context, value, child) {
opacity: value, return Opacity(
child: Transform.translate( opacity: value,
offset: Offset(0, 30 * (1 - value)), child: Transform.translate(
child: child, offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
context.tr("enter_code_to_join"),
style: const TextStyle(
fontSize: 16,
color: Colors.black54,
height: 1.4,
),
textAlign: TextAlign.center,
), ),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 30),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.08),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
LucideIcons.keySquare,
color: AppColors.primaryBlue,
size: 24,
),
const SizedBox(width: 12),
Text(
context.tr("enter_room_code"),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 25),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
),
child: GlobalTextField(
controller: controller.codeController,
hintText: context.tr("room_code_hint"),
textInputType: TextInputType.text,
forceUpperCase: true,
),
),
const SizedBox(height: 30),
GlobalButton(
text: context.tr("join_quiz_now"),
onPressed: () => controller.joinRoom(context),
),
],
), ),
), ),
),
const SizedBox(height: 30), const SizedBox(height: 40),
],
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 1000),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 30 * (1 - value)),
child: child,
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 30),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.08),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
LucideIcons.keySquare,
color: AppColors.primaryBlue,
size: 24,
),
const SizedBox(width: 12),
Text(
context.tr("enter_room_code"),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 25),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
),
child: GlobalTextField(
controller: controller.codeController,
hintText: context.tr("room_code_hint"),
textInputType: TextInputType.text,
forceUpperCase: true,
),
),
const SizedBox(height: 30),
GlobalButton(
text: context.tr("join_quiz_now"),
onPressed: () => controller.joinRoom(context),
),
],
),
),
),
const SizedBox(height: 30),
],
),
), ),
), ),
), ),

View File

@ -102,10 +102,12 @@ class LoginController extends GetxController {
_userController.setUserFromEntity(userEntity); _userController.setUserFromEntity(userEntity);
_userStorageService.isLogged = true; _userStorageService.isLogged = true;
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
Get.offAllNamed(AppRoutes.mainPage); Get.offAllNamed(AppRoutes.mainPage);
} catch (e, stackTrace) { } catch (e, stackTrace) {
logC.e(e, stackTrace: stackTrace); logC.e(e, stackTrace: stackTrace);
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
CustomNotification.error(title: "Gagal", message: "Periksa kembali email dan kata sandi Anda"); CustomNotification.error(title: "Gagal", message: "Periksa kembali email dan kata sandi Anda");
} }
} }
@ -117,11 +119,13 @@ class LoginController extends GetxController {
} }
try { try {
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoading(Get.overlayContext!);
isLoading.value = true;
final user = await _googleAuthService.signIn(); final user = await _googleAuthService.signIn();
if (user == null) { if (user == null) {
Get.snackbar("Kesalahan", "Masuk dengan Google dibatalkan"); Get.snackbar("Kesalahan", "Masuk dengan Google dibatalkan");
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
return; return;
} }
@ -130,6 +134,7 @@ class LoginController extends GetxController {
Get.snackbar("Kesalahan", "Tidak menerima ID Token dari Google"); Get.snackbar("Kesalahan", "Tidak menerima ID Token dari Google");
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
return; return;
} }
@ -140,13 +145,20 @@ class LoginController extends GetxController {
_userController.setUserFromEntity(userEntity); _userController.setUserFromEntity(userEntity);
_userStorageService.isLogged = true; _userStorageService.isLogged = true;
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
Get.offAllNamed(AppRoutes.mainPage); Get.offAllNamed(AppRoutes.mainPage);
} catch (e, stackTrace) { } catch (e, stackTrace) {
logC.e("Google Sign-In Error: $e", stackTrace: stackTrace); logC.e("Google Sign-In Error: $e", stackTrace: stackTrace);
Get.snackbar("Error", "Google sign-in error"); Get.snackbar("Error", "Google sign-in error");
CustomFloatingLoading.hideLoading();
isLoading.value = false;
} }
} }
void onGoBack() {
if (!isLoading.value) Get.back();
}
void goToRegsPage() => Get.toNamed(AppRoutes.registerPage); void goToRegsPage() => Get.toNamed(AppRoutes.registerPage);
UserEntity _convertLoginResponseToUserEntity(LoginResponseModel response) { UserEntity _convertLoginResponseToUserEntity(LoginResponseModel response) {

View File

@ -15,70 +15,74 @@ class LoginView extends GetView<LoginController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
backgroundColor: AppColors.background, canPop: false,
body: SafeArea( onPopInvokedWithResult: (didPop, result) => controller.onGoBack(),
child: Padding( child: Scaffold(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), backgroundColor: AppColors.background,
child: ListView( body: SafeArea(
children: [ child: Padding(
const SizedBox(height: 40), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
const AppName(), child: ListView(
const SizedBox(height: 40), children: [
LabelTextField( const SizedBox(height: 40),
label: context.tr("log_in"), const AppName(),
fontSize: 28, const SizedBox(height: 40),
fontWeight: FontWeight.bold, LabelTextField(
color: Color(0xFF172B4D), label: context.tr("log_in"),
), fontSize: 28,
const SizedBox(height: 24), fontWeight: FontWeight.bold,
LabelTextField( color: Color(0xFF172B4D),
label: context.tr("email"),
color: Color(0xFF6B778C),
fontSize: 14,
),
const SizedBox(height: 6),
GlobalTextField(
controller: controller.emailController,
hintText: context.tr("enter_your_email"),
),
const SizedBox(height: 20),
LabelTextField(
label: context.tr("password"),
color: Color(0xFF6B778C),
fontSize: 14,
),
const SizedBox(height: 6),
Obx(
() => GlobalTextField(
controller: controller.passwordController,
isPassword: true,
obscureText: controller.isPasswordHidden.value,
onToggleVisibility: controller.togglePasswordVisibility,
hintText: context.tr("enter_your_password"),
), ),
), const SizedBox(height: 24),
const SizedBox(height: 32), LabelTextField(
Obx(() => GlobalButton( label: context.tr("email"),
onPressed: controller.loginWithEmail, color: Color(0xFF6B778C),
text: context.tr("sign_in"), fontSize: 14,
type: controller.isButtonEnabled.value, ),
)), const SizedBox(height: 6),
const SizedBox(height: 24), GlobalTextField(
LabelTextField( controller: controller.emailController,
label: context.tr("or"), hintText: context.tr("enter_your_email"),
alignment: Alignment.center, ),
color: Color(0xFF6B778C), const SizedBox(height: 20),
), LabelTextField(
const SizedBox(height: 24), label: context.tr("password"),
GoogleButton( color: Color(0xFF6B778C),
onPress: controller.loginWithGoogle, fontSize: 14,
), ),
const SizedBox(height: 32), const SizedBox(height: 6),
RegisterTextButton( Obx(
onTap: controller.goToRegsPage, () => GlobalTextField(
), controller: controller.passwordController,
], isPassword: true,
obscureText: controller.isPasswordHidden.value,
onToggleVisibility: controller.togglePasswordVisibility,
hintText: context.tr("enter_your_password"),
),
),
const SizedBox(height: 32),
Obx(() => GlobalButton(
onPressed: controller.loginWithEmail,
text: context.tr("sign_in"),
type: controller.isButtonEnabled.value,
)),
const SizedBox(height: 24),
LabelTextField(
label: context.tr("or"),
alignment: Alignment.center,
color: Color(0xFF6B778C),
),
const SizedBox(height: 24),
GoogleButton(
onPress: controller.loginWithGoogle,
),
const SizedBox(height: 32),
RegisterTextButton(
onTap: controller.goToRegsPage,
),
],
),
), ),
), ),
), ),

View File

@ -71,7 +71,6 @@ class ProfileController extends GetxController {
birthDate.value = _userController.userData?.birthDate ?? ""; birthDate.value = _userController.userData?.birthDate ?? "";
phoneNumber.value = _userController.userData?.phone ?? ""; phoneNumber.value = _userController.userData?.phone ?? "";
joinDate.value = _userController.userData?.createdAt ?? ""; joinDate.value = _userController.userData?.createdAt ?? "";
} catch (e, stackTrace) { } catch (e, stackTrace) {
logC.e("Failed to load user profile data: $e", stackTrace: stackTrace); logC.e("Failed to load user profile data: $e", stackTrace: stackTrace);
} }
@ -114,9 +113,9 @@ class ProfileController extends GetxController {
} }
void editProfile() async { void editProfile() async {
final bool resultUpdate = await Get.toNamed(AppRoutes.updateProfilePage); final resultUpdate = await Get.toNamed(AppRoutes.updateProfilePage);
if (resultUpdate) { if (resultUpdate == true) {
loadUserProfileData(); loadUserProfileData();
} }
} }

View File

@ -29,6 +29,8 @@ class UpdateProfileController extends GetxController {
var selectedLocale = 'en-US'.obs; var selectedLocale = 'en-US'.obs;
RxBool isLoading = false.obs;
final Map<String, String> localeMap = { final Map<String, String> localeMap = {
'English': 'en-US', 'English': 'en-US',
'Indonesian': 'id-ID', 'Indonesian': 'id-ID',
@ -78,6 +80,7 @@ class UpdateProfileController extends GetxController {
try { try {
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoading(Get.overlayContext!);
isLoading.value = true;
final isSuccessUpdate = await _userService.updateProfileData( final isSuccessUpdate = await _userService.updateProfileData(
_userController.userData!.id, _userController.userData!.id,
nameController.text.trim(), nameController.text.trim(),
@ -111,11 +114,13 @@ class UpdateProfileController extends GetxController {
} }
} }
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
Get.back(result: true); Get.back(result: true);
CustomNotification.success(title: "Success", message: "Profile updated successfully"); CustomNotification.success(title: "Success", message: "Profile updated successfully");
} catch (e) { } catch (e) {
CustomNotification.success(title: "something wrong", message: "failed to update profile"); CustomNotification.success(title: "something wrong", message: "failed to update profile");
isLoading.value = false;
logC.e(e); logC.e(e);
} }
} }
@ -124,4 +129,8 @@ class UpdateProfileController extends GetxController {
final regex = RegExp(r'^([0-2][0-9]|(3)[0-1])\-((0[1-9])|(1[0-2]))\-\d{4}$'); final regex = RegExp(r'^([0-2][0-9]|(3)[0-1])\-((0[1-9])|(1[0-2]))\-\d{4}$');
return regex.hasMatch(date); return regex.hasMatch(date);
} }
void onGoBack() {
if (!isLoading.value) Get.back();
}
} }

View File

@ -11,58 +11,62 @@ import 'package:quiz_app/feature/profile/controller/update_profile_controller.da
class UpdateProfilePage extends GetView<UpdateProfileController> { class UpdateProfilePage extends GetView<UpdateProfileController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
backgroundColor: AppColors.background2, canPop: false,
appBar: AppBar( onPopInvokedWithResult: (didPop, result) => controller.onGoBack(),
child: Scaffold(
backgroundColor: AppColors.background2, backgroundColor: AppColors.background2,
title: Text('Update Profile'), appBar: AppBar(
centerTitle: true, backgroundColor: AppColors.background2,
), title: Text('Update Profile'),
body: Padding( centerTitle: true,
padding: const EdgeInsets.all(16.0), ),
child: ListView( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
LabelTextField(label: "Name"), child: ListView(
GlobalTextField(controller: controller.nameController), children: [
SizedBox(height: 16), LabelTextField(label: "Name"),
LabelTextField(label: "Phone"), GlobalTextField(controller: controller.nameController),
GlobalTextField( SizedBox(height: 16),
controller: controller.phoneController, LabelTextField(label: "Phone"),
hintText: 'Enter your phone number', GlobalTextField(
), controller: controller.phoneController,
SizedBox(height: 16), hintText: 'Enter your phone number',
LabelTextField(label: "Birth Date"), ),
GlobalTextField( SizedBox(height: 16),
controller: controller.birthDateController, LabelTextField(label: "Birth Date"),
hintText: 'Enter your birth date', GlobalTextField(
), controller: controller.birthDateController,
SizedBox(height: 16), hintText: 'Enter your birth date',
LabelTextField(label: "Locale"), ),
Obx(() => GlobalDropdownField<String>( SizedBox(height: 16),
value: controller.selectedLocale.value, LabelTextField(label: "Locale"),
items: controller.localeMap.entries.map<DropdownMenuItem<String>>((entry) { Obx(() => GlobalDropdownField<String>(
return DropdownMenuItem<String>( value: controller.selectedLocale.value,
value: entry.value, items: controller.localeMap.entries.map<DropdownMenuItem<String>>((entry) {
child: Text(entry.key), // Display country name return DropdownMenuItem<String>(
); value: entry.value,
}).toList(), child: Text(entry.key), // Display country name
onChanged: (String? newValue) { );
if (newValue != null) { }).toList(),
controller.selectedLocale.value = newValue; onChanged: (String? newValue) {
final parts = newValue.split('-'); if (newValue != null) {
if (parts.length == 2) { controller.selectedLocale.value = newValue;
Get.updateLocale(Locale(parts[0], parts[1])); final parts = newValue.split('-');
} else { if (parts.length == 2) {
Get.updateLocale(Locale(newValue)); Get.updateLocale(Locale(parts[0], parts[1]));
} else {
Get.updateLocale(Locale(newValue));
}
} }
} },
}, )),
)), SizedBox(height: 32),
SizedBox(height: 32), Center(
Center( child: GlobalButton(text: tr("save_changes"), onPressed: controller.saveProfile),
child: GlobalButton(text: tr("save_changes"), onPressed: controller.saveProfile), ),
), ],
], ),
), ),
), ),
); );

View File

@ -35,6 +35,8 @@ class QuizCreationController extends GetxController {
RxInt currentDuration = 30.obs; RxInt currentDuration = 30.obs;
RxBool isLoading = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -199,7 +201,7 @@ class QuizCreationController extends GetxController {
void onBack(BuildContext context) { void onBack(BuildContext context) {
if (quizData.length <= 1) { if (quizData.length <= 1) {
Navigator.pop(context); Get.back();
} else { } else {
AppDialog.showExitConfirmationDialog(context); AppDialog.showExitConfirmationDialog(context);
} }
@ -240,50 +242,67 @@ class QuizCreationController extends GetxController {
return; return;
} }
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoading(Get.overlayContext!);
isLoading.value = true;
try { try {
BaseResponseModel<List<RawQuizModel>> response = await _quizService.createQuizAuto(inputSentenceTC.text); BaseResponseModel<List<RawQuizModel>> response = await _quizService.createQuizAuto(inputSentenceTC.text);
if (response.data != null) { if (response.data != null && response.data!.isNotEmpty) {
final previousLength = quizData.length; // Check if we should remove the initial empty question
bool shouldRemoveInitial = quizData.length == 1 && quizData[0].question == null && quizData[0].answer == null;
if (previousLength == 1) quizData.removeAt(0); if (shouldRemoveInitial) {
quizData.clear();
}
for (final i in response.data!) { // Add new questions
for (final quizItem in response.data!) {
QuestionType type = QuestionType.fillTheBlank; QuestionType type = QuestionType.fillTheBlank;
if (i.answer.toString().toLowerCase() == 'true' || i.answer.toString().toLowerCase() == 'false') { if (quizItem.answer.toString().toLowerCase() == 'true' || quizItem.answer.toString().toLowerCase() == 'false') {
type = QuestionType.trueOrFalse; type = QuestionType.trueOrFalse;
} }
quizData.add(QuestionData( quizData.add(QuestionData(
index: quizData.length + 1, index: quizData.length + 1,
question: i.qustion, question: quizItem.qustion,
answer: i.answer, answer: quizItem.answer,
type: type, type: type,
)); ));
} }
if (response.data!.isNotEmpty) { // Set the selected index to the first newly added question
selectedQuizIndex.value = previousLength; if (shouldRemoveInitial) {
selectedQuizIndex.value = 0;
} else {
// If we didn't remove initial data, select the first new question
selectedQuizIndex.value = quizData.length - response.data!.length;
}
// Update UI with the selected question data
if (selectedQuizIndex.value < quizData.length) {
final data = quizData[selectedQuizIndex.value]; final data = quizData[selectedQuizIndex.value];
questionTC.text = data.question ?? ""; questionTC.text = data.question ?? "";
answerTC.text = data.answer ?? ""; answerTC.text = data.answer ?? "";
currentDuration.value = data.duration; currentDuration.value = data.duration;
currentQuestionType.value = data.type ?? QuestionType.fillTheBlank; currentQuestionType.value = data.type ?? QuestionType.fillTheBlank;
return;
} }
} }
} catch (e) { } catch (e) {
logC.e("Error while generating quiz: $e"); logC.e("Error while generating quiz: $e");
CustomFloatingLoading.hideLoading();
} finally { } finally {
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
isGenerate.value = false; isGenerate.value = false;
inputSentenceTC.text = ""; inputSentenceTC.text = "";
if (quizData.isNotEmpty && selectedQuizIndex.value == 0) { if (quizData.isNotEmpty && selectedQuizIndex.value >= quizData.length) {
final data = quizData[0]; selectedQuizIndex.value = 0;
}
if (quizData.isNotEmpty) {
final data = quizData[selectedQuizIndex.value];
questionTC.text = data.question ?? ""; questionTC.text = data.question ?? "";
answerTC.text = data.answer ?? ""; answerTC.text = data.answer ?? "";
currentDuration.value = data.duration; currentDuration.value = data.duration;
@ -291,4 +310,8 @@ class QuizCreationController extends GetxController {
} }
} }
} }
onGoBack(BuildContext context, bool didPop) {
if (!isLoading.value) onBack(context);
}
} }

View File

@ -11,35 +11,39 @@ class QuizCreationView extends GetView<QuizCreationController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
backgroundColor: AppColors.background, canPop: false,
appBar: AppBar( onPopInvokedWithResult: (didPop, result) => controller.onGoBack(context, didPop),
child: Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
elevation: 0, appBar: AppBar(
title: Text( backgroundColor: AppColors.background,
context.tr('create_quiz_title'), elevation: 0,
style: const TextStyle( title: Text(
fontWeight: FontWeight.bold, context.tr('create_quiz_title'),
color: AppColors.darkText, style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.darkText,
),
), ),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: AppColors.darkText),
onPressed: () => controller.onBack(context),
),
centerTitle: true,
), ),
leading: IconButton( body: SafeArea(
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: AppColors.darkText), child: Padding(
onPressed: () => controller.onBack(context), padding: const EdgeInsets.all(20.0),
), child: SingleChildScrollView(
centerTitle: true, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
body: SafeArea( children: [
child: Padding( _buildModeSelector(context),
padding: const EdgeInsets.all(20.0), const SizedBox(height: 20),
child: SingleChildScrollView( Obx(() => controller.isGenerate.value ? const GenerateComponent() : const CustomQuestionComponent()),
child: Column( ],
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [
_buildModeSelector(context),
const SizedBox(height: 20),
Obx(() => controller.isGenerate.value ? const GenerateComponent() : const CustomQuestionComponent()),
],
), ),
), ),
), ),

View File

@ -180,6 +180,10 @@ class QuizPreviewController extends GetxController {
subjectIndex.value = index; subjectIndex.value = index;
} }
void onBack() {
if (!isLoading.value) Get.back();
}
@override @override
void onClose() { void onClose() {
titleController.dispose(); titleController.dispose();

View File

@ -34,6 +34,7 @@ class SubjectDropdownComponent extends StatelessWidget {
} }
} }
}, },
dropdownColor: Colors.white,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,

View File

@ -14,13 +14,17 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
backgroundColor: AppColors.background, canPop: false,
appBar: _buildAppBar(context), onPopInvokedWithResult: (didPop, result) => controller.onBack(),
body: SafeArea( child: Scaffold(
child: Padding( backgroundColor: AppColors.background,
padding: const EdgeInsets.all(20.0), appBar: _buildAppBar(context),
child: _buildContent(context), body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: _buildContent(context),
),
), ),
), ),
); );

View File

@ -26,6 +26,8 @@ class RegisterController extends GetxController {
var isPasswordHidden = true.obs; var isPasswordHidden = true.obs;
var isConfirmPasswordHidden = true.obs; var isConfirmPasswordHidden = true.obs;
RxBool isLoading = false.obs;
@override @override
void onReady() { void onReady() {
if (!_connectionService.isCurrentlyConnected) { if (!_connectionService.isCurrentlyConnected) {
@ -82,6 +84,7 @@ class RegisterController extends GetxController {
try { try {
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoading(Get.overlayContext!);
isLoading.value = true;
await _authService.register( await _authService.register(
RegisterRequestModel( RegisterRequestModel(
email: email, email: email,
@ -94,9 +97,11 @@ class RegisterController extends GetxController {
Get.back(); Get.back();
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
CustomNotification.success(title: "Pendaftaran Berhasil", message: "Akun berhasil dibuat"); CustomNotification.success(title: "Pendaftaran Berhasil", message: "Akun berhasil dibuat");
} catch (e) { } catch (e) {
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoading();
isLoading.value = false;
String errorMessage = e.toString().replaceFirst("Exception: ", ""); String errorMessage = e.toString().replaceFirst("Exception: ", "");