Compare commits

..

No commits in common. "0a138793ae11a6b968e4c594cf155b2521a32c05" and "655103247e92bfe00c50356101bbd5e58ea81af7" have entirely different histories.

55 changed files with 608 additions and 1087 deletions

View File

@ -38,8 +38,8 @@
"library_title": "Quiz Library", "library_title": "Quiz Library",
"library_description": "A collection of quiz questions created for study.", "library_description": "A collection of quiz questions created for study.",
"no_quiz_available": "No quizzes available yet.", "no_quiz_available": "No quizzes available yet.",
"quiz_count_label": "Quiz", "quiz_count_label": "Quizzes",
"quiz_count_named": "{total} Quiz", "quiz_count_named": "{total} Quizzes",
"history_title": "Quiz History", "history_title": "Quiz History",
"history_subtitle": "Review the quizzes you've taken", "history_subtitle": "Review the quizzes you've taken",
@ -135,23 +135,5 @@
"second": "{} s", "second": "{} s",
"minute": "{} m", "minute": "{} m",
"hour": "{} h" "hour": "{} h"
}, }
"get_ready": "Get Ready",
"quiz_starting_soon": "Quiz Starting Soon",
"waiting_room": {
"title": "Waiting Room",
"participants_joined": "Participants Joined:",
"leave_room": "Leave Room",
"session_code": "Session Code:",
"copy_code": "Copy Code",
"quiz_info": "Quiz Information:",
"quiz_title": "Title",
"quiz_description": "Description",
"quiz_total_question": "Total Questions",
"quiz_duration": "Duration"
},
"save_changes" : "Save Changes"
} }

View File

@ -119,22 +119,5 @@
"second": "{} d", "second": "{} d",
"minute": "{} m", "minute": "{} m",
"hour": "{} j" "hour": "{} j"
}, }
"get_ready": "Bersiaplah",
"quiz_starting_soon": "Kuis akan segera dimulai",
"waiting_room": {
"title": "Ruang Tunggu",
"participants_joined": "Peserta Bergabung:",
"leave_room": "Keluar dari Ruangan",
"session_code": "Kode Sesi:",
"copy_code": "Salin Kode",
"quiz_info": "Informasi Kuis:",
"quiz_title": "Judul",
"quiz_description": "Deskripsi",
"quiz_total_question": "Total Pertanyaan",
"quiz_duration": "Durasi"
},
"save_changes": "Simpan Perubahan"
} }

View File

@ -121,21 +121,5 @@
"second": "{} s", "second": "{} s",
"minute": "{} m", "minute": "{} m",
"hour": "{} j" "hour": "{} j"
}, }
"get_ready": "Bersedia",
"quiz_starting_soon": "Kuiz akan bermula sebentar lagi",
"waiting_room": {
"title": "Bilik Menunggu",
"participants_joined": "Peserta Telah Sertai:",
"leave_room": "Tinggalkan Bilik",
"session_code": "Kod Sesi:",
"copy_code": "Salin Kod",
"quiz_info": "Maklumat Kuiz:",
"quiz_title": "Tajuk",
"quiz_description": "Penerangan",
"quiz_total_question": "Jumlah Soalan",
"quiz_duration": "Tempoh"
},
"save_changes": "Simpan Perubahan"
} }

View File

@ -12,7 +12,7 @@ class MyApp extends StatelessWidget {
return GetMaterialApp( return GetMaterialApp(
title: 'Quiz App', title: 'Quiz App',
locale: Get.locale ?? context.locale, locale: Get.locale ?? context.locale,
fallbackLocale: const Locale('id', 'ID'), fallbackLocale: const Locale('en', 'US'),
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
initialBinding: InitialBindings(), initialBinding: InitialBindings(),

View File

@ -1,5 +0,0 @@
extension StringCasingExtension on String {
String toTitleCase() {
return split(' ').map((word) => word.isNotEmpty ? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}' : '').join(' ');
}
}

View File

@ -119,88 +119,4 @@ class AppDialog {
}, },
); );
} }
static Future<bool?> showConfirmationDialog(
BuildContext context, {
required String title,
required String message,
String cancelText = "Batal",
String confirmText = "Yakin",
Color confirmColor = AppColors.primaryBlue,
}) async {
return showDialog<bool>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return Dialog(
backgroundColor: AppColors.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.darkText,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
fontSize: 14,
color: AppColors.softGrayText,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context, false),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColors.primaryBlue,
side: const BorderSide(color: AppColors.primaryBlue),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: Text(cancelText),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: confirmColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: Text(confirmText),
),
),
],
)
],
),
),
);
},
);
}
} }

View File

@ -1,6 +1,6 @@
class APIEndpoint { class APIEndpoint {
// static const String baseUrl = "http://192.168.1.13:5000"; static const String baseUrl = "http://192.168.1.18:5000";
static const String baseUrl = "http://103.193.178.121:5000"; // static const String baseUrl = "http://103.193.178.121:5000";
static const String api = "$baseUrl/api"; static const String api = "$baseUrl/api";
static const String login = "/login"; static const String login = "/login";

View File

@ -1,34 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomFloatingLoading { class CustomFloatingLoading {
static OverlayEntry? _overlayEntry; static void showLoadingDialog(BuildContext context) {
showDialog(
static void showLoading(BuildContext context) { context: context,
if (_overlayEntry != null) return; barrierDismissible: false,
barrierColor: Colors.black.withValues(alpha: 0.3),
_overlayEntry = OverlayEntry( builder: (BuildContext context) {
builder: (_) => Stack( return PopScope(
children: [ canPop: false,
ModalBarrier( child: const Center(
dismissible: false, child: CircularProgressIndicator(),
color: Colors.black.withValues(alpha: 0.5),
), ),
const Center( );
child: CircularProgressIndicator( },
color: Colors.white,
),
),
],
),
); );
Overlay.of(context).insert(_overlayEntry!);
} }
static void hideLoading() { static void hideLoadingDialog(BuildContext context) {
if (_overlayEntry?.mounted == true) { Navigator.of(context).pop();
_overlayEntry?.remove();
}
_overlayEntry = null;
} }
} }

View File

@ -40,8 +40,7 @@ class UserEntity {
'pic_url': picUrl, 'pic_url': picUrl,
'birth_date': birthDate, 'birth_date': birthDate,
'locale': locale, 'locale': locale,
'phone': phone, "create_at": createdAt,
"created_at": createdAt,
}; };
} }
} }

View File

@ -3,7 +3,6 @@ import 'package:quiz_app/data/models/user/user_model.dart';
class SessionInfo { class SessionInfo {
final String id; final String id;
final String sessionCode; final String sessionCode;
final String roomName;
final String quizId; final String quizId;
final String hostId; final String hostId;
final DateTime createdAt; final DateTime createdAt;
@ -17,7 +16,6 @@ class SessionInfo {
SessionInfo({ SessionInfo({
required this.id, required this.id,
required this.sessionCode, required this.sessionCode,
required this.roomName,
required this.quizId, required this.quizId,
required this.hostId, required this.hostId,
required this.createdAt, required this.createdAt,
@ -33,7 +31,6 @@ class SessionInfo {
return SessionInfo( return SessionInfo(
id: json['id'], id: json['id'],
sessionCode: json['session_code'], sessionCode: json['session_code'],
roomName: json["room_name"],
quizId: json['quiz_id'], quizId: json['quiz_id'],
hostId: json['host_id'], hostId: json['host_id'],
createdAt: DateTime.parse(json['created_at']), createdAt: DateTime.parse(json['created_at']),

View File

@ -1,13 +1,11 @@
class SessionRequestModel { class SessionRequestModel {
final String quizId; final String quizId;
final String hostId; final String hostId;
final String roomName;
final int limitParticipan; final int limitParticipan;
SessionRequestModel({ SessionRequestModel({
required this.quizId, required this.quizId,
required this.hostId, required this.hostId,
required this.roomName,
required this.limitParticipan, required this.limitParticipan,
}); });
@ -15,7 +13,6 @@ class SessionRequestModel {
return SessionRequestModel( return SessionRequestModel(
quizId: json['quiz_id'], quizId: json['quiz_id'],
hostId: json['host_id'], hostId: json['host_id'],
roomName: json['room_name'],
limitParticipan: json['limit_participan'], limitParticipan: json['limit_participan'],
); );
} }
@ -24,7 +21,6 @@ class SessionRequestModel {
return { return {
'quiz_id': quizId, 'quiz_id': quizId,
'host_id': hostId, 'host_id': hostId,
'room_name': roomName,
'limit_participan': limitParticipan, 'limit_participan': limitParticipan,
}; };
} }

View File

@ -42,7 +42,7 @@ class ConnectionService extends GetxService {
isConnected.value = results.any((result) => result != ConnectivityResult.none); isConnected.value = results.any((result) => result != ConnectivityResult.none);
} }
Future<bool> isHaveConnection() async { Future<bool> checkConnection() async {
final result = await _connectivity.checkConnectivity(); final result = await _connectivity.checkConnectivity();
return !result.contains(ConnectivityResult.none); return !result.contains(ConnectivityResult.none);
} }

View File

@ -1,7 +1,4 @@
import 'dart:ui';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.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';
@ -85,8 +82,7 @@ class QuizService extends GetxService {
Future<BaseResponseModel<List<QuizListingModel>>?> populerQuiz({int page = 1, int amount = 3}) async { Future<BaseResponseModel<List<QuizListingModel>>?> populerQuiz({int page = 1, int amount = 3}) async {
try { try {
Locale locale = Localizations.localeOf(Get.context!); final response = await dio.get("${APIEndpoint.quizPopuler}?page=$page&limit=$amount");
final response = await dio.get("${APIEndpoint.quizPopuler}?page=$page&limit=$amount&lang_code=${locale.languageCode}");
if (response.statusCode == 200) { if (response.statusCode == 200) {
final parsedResponse = BaseResponseModel<List<QuizListingModel>>.fromJson( final parsedResponse = BaseResponseModel<List<QuizListingModel>>.fromJson(
@ -106,8 +102,7 @@ class QuizService extends GetxService {
Future<BaseResponseModel<List<QuizListingModel>>?> recommendationQuiz({int page = 1, int amount = 3, String userId = ""}) async { Future<BaseResponseModel<List<QuizListingModel>>?> recommendationQuiz({int page = 1, int amount = 3, String userId = ""}) async {
try { try {
Locale locale = Localizations.localeOf(Get.context!); final response = await dio.get("${APIEndpoint.quizRecommendation}?page=$page&limit=$amount&user_id$userId");
final response = await dio.get("${APIEndpoint.quizRecommendation}?page=$page&limit=$amount&user_id=$userId&lang_code=${locale.languageCode}");
if (response.statusCode == 200) { if (response.statusCode == 200) {
final parsedResponse = BaseResponseModel<List<QuizListingModel>>.fromJson( final parsedResponse = BaseResponseModel<List<QuizListingModel>>.fromJson(

View File

@ -17,7 +17,11 @@ class SessionService extends GetxService {
Future<BaseResponseModel<SessionResponseModel>?> createSession(SessionRequestModel data) async { Future<BaseResponseModel<SessionResponseModel>?> createSession(SessionRequestModel data) async {
try { try {
final response = await _dio.post(APIEndpoint.session, data: data.toJson()); final response = await _dio.post(APIEndpoint.session, data: {
'quiz_id': data.quizId,
'host_id': data.hostId,
'limit_participan': data.limitParticipan,
});
if (response.statusCode != 201) { if (response.statusCode != 201) {
return null; return null;
} }

View File

@ -1,5 +1,4 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart'; import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart';
@ -9,11 +8,6 @@ class DetailQuizBinding extends Bindings {
if (!Get.isRegistered<QuizService>()) { if (!Get.isRegistered<QuizService>()) {
Get.lazyPut<QuizService>(() => QuizService()); Get.lazyPut<QuizService>(() => QuizService());
} }
Get.lazyPut<DetailQuizController>( Get.lazyPut<DetailQuizController>(() => DetailQuizController(Get.find<QuizService>()));
() => DetailQuizController(
Get.find<QuizService>(),
Get.find<ConnectionService>(),
),
);
} }
} }

View File

@ -1,20 +1,17 @@
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/core/helper/connection_check.dart';
import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
class DetailQuizController extends GetxController { class DetailQuizController extends GetxController {
final QuizService _quizService; final QuizService _quizService;
final ConnectionService _connectionService;
DetailQuizController(this._quizService, this._connectionService); DetailQuizController(this._quizService);
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
QuizData? data; late QuizData data;
@override @override
void onInit() { void onInit() {
@ -24,11 +21,6 @@ class DetailQuizController extends GetxController {
void loadData() async { void loadData() async {
final quizId = Get.arguments as String; final quizId = Get.arguments as String;
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
isLoading.value = false;
return;
}
getQuizData(quizId); getQuizData(quizId);
} }
@ -40,11 +32,5 @@ class DetailQuizController extends GetxController {
isLoading.value = false; isLoading.value = false;
} }
void goToPlayPage() { void goToPlayPage() => Get.toNamed(AppRoutes.playQuizPage, arguments: data);
if (!_connectionService.isCurrentlyConnected) {
ConnectionNotification.noInternedConnection();
return;
}
Get.toNamed(AppRoutes.playQuizPage, arguments: data);
}
} }

View File

@ -30,76 +30,70 @@ class DetailQuizView extends GetView<DetailQuizController> {
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Obx(() { child: Obx(
if (controller.isLoading.value) { () => controller.isLoading.value
return const Center(child: LoadingWidget()); ? const Center(child: LoadingWidget())
} : SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Section
Text(
controller.data.title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppColors.darkText,
),
),
const SizedBox(height: 8),
Text(
controller.data.description ?? "",
style: const TextStyle(
fontSize: 14,
color: AppColors.softGrayText,
),
),
const SizedBox(height: 16),
Row(
children: [
const Icon(Icons.calendar_today_rounded, size: 16, color: AppColors.softGrayText),
const SizedBox(width: 6),
Text(
controller.data.date ?? "",
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
),
const SizedBox(width: 12),
const Icon(Icons.timer_rounded, size: 16, color: AppColors.softGrayText),
const SizedBox(width: 6),
Text(
'${controller.data.limitDuration ~/ 60} ${tr('minutes_suffix')}',
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
),
],
),
const SizedBox(height: 20),
if (controller.data == null) { GlobalButton(text: tr('start_quiz'), onPressed: controller.goToPlayPage),
return const Center(child: Text("Tidak Ditemukan")); const SizedBox(height: 20),
}
return SingleChildScrollView( const Divider(thickness: 1.2, color: AppColors.borderLight),
child: Column( const SizedBox(height: 20),
crossAxisAlignment: CrossAxisAlignment.start,
children: [ // Soal Section
// Header Section ListView.builder(
Text( shrinkWrap: true,
controller.data!.title, physics: const NeverScrollableScrollPhysics(),
style: const TextStyle( itemCount: controller.data.questionListings.length,
fontSize: 22, itemBuilder: (context, index) {
fontWeight: FontWeight.bold, final question = controller.data.questionListings[index];
color: AppColors.darkText, return _buildQuestionItem(question, index + 1);
},
),
],
), ),
), ),
const SizedBox(height: 8), ),
Text(
controller.data!.description ?? "",
style: const TextStyle(
fontSize: 14,
color: AppColors.softGrayText,
),
),
const SizedBox(height: 16),
Row(
children: [
const Icon(Icons.calendar_today_rounded, size: 16, color: AppColors.softGrayText),
const SizedBox(width: 6),
Text(
controller.data!.date ?? "",
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
),
const SizedBox(width: 12),
const Icon(Icons.timer_rounded, size: 16, color: AppColors.softGrayText),
const SizedBox(width: 6),
Text(
'${controller.data!.limitDuration ~/ 60} ${tr('minutes_suffix')}',
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
),
],
),
const SizedBox(height: 20),
GlobalButton(text: tr('start_quiz'), onPressed: controller.goToPlayPage),
const SizedBox(height: 20),
const Divider(thickness: 1.2, color: AppColors.borderLight),
const SizedBox(height: 20),
// Soal Section
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.data!.questionListings.length,
itemBuilder: (context, index) {
final question = controller.data!.questionListings[index];
return _buildQuestionItem(question, index + 1);
},
),
],
),
);
}),
), ),
), ),
); );

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/history_service.dart'; import 'package:quiz_app/data/services/history_service.dart';
import 'package:quiz_app/feature/history/controller/history_controller.dart'; import 'package:quiz_app/feature/history/controller/history_controller.dart';
@ -8,12 +7,6 @@ class HistoryBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<HistoryService>(() => HistoryService()); Get.lazyPut<HistoryService>(() => HistoryService());
Get.lazyPut( Get.lazyPut(() => HistoryController(Get.find<HistoryService>(), Get.find<UserController>()));
() => HistoryController(
Get.find<HistoryService>(),
Get.find<UserController>(),
Get.find<ConnectionService>(),
),
);
} }
} }

View File

@ -1,21 +1,14 @@
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/core/helper/connection_check.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/history/quiz_history.dart'; import 'package:quiz_app/data/models/history/quiz_history.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/history_service.dart'; import 'package:quiz_app/data/services/history_service.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
final HistoryService _historyService; final HistoryService _historyService;
final UserController _userController; final UserController _userController;
final ConnectionService _connectionService;
HistoryController( HistoryController(this._historyService, this._userController);
this._historyService,
this._userController,
this._connectionService,
);
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
@ -24,15 +17,10 @@ class HistoryController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
loadHistory(); loadDummyHistory();
} }
void loadHistory() async { void loadDummyHistory() async {
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return;
}
historyList.value = await _historyService.getHistory(_userController.userData!.id) ?? []; historyList.value = await _historyService.getHistory(_userController.userData!.id) ?? [];
isLoading.value = false; isLoading.value = false;
} }

View File

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

View File

@ -16,14 +16,11 @@ class HistoryView extends GetView<HistoryController> {
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(context.tr("history_title"), style: AppTextStyles.title.copyWith(fontSize: 24)),
context.tr("history_title"),
style: AppTextStyles.title.copyWith(fontSize: 24),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
context.tr("history_subtitle"), context.tr("history_subtitle"),

View File

@ -1,6 +1,4 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/subject_service.dart'; import 'package:quiz_app/data/services/subject_service.dart';
import 'package:quiz_app/feature/home/controller/home_controller.dart'; import 'package:quiz_app/feature/home/controller/home_controller.dart';
@ -12,10 +10,8 @@ class HomeBinding extends Bindings {
Get.lazyPut<SubjectService>(() => SubjectService()); Get.lazyPut<SubjectService>(() => SubjectService());
Get.lazyPut<HomeController>( Get.lazyPut<HomeController>(
() => HomeController( () => HomeController(
Get.find<UserController>(),
Get.find<QuizService>(), Get.find<QuizService>(),
Get.find<SubjectService>(), Get.find<SubjectService>(),
Get.find<ConnectionService>(),
), ),
); );
} }

View File

@ -1,28 +1,24 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/enums/listing_type.dart'; import 'package:quiz_app/app/const/enums/listing_type.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
import 'package:quiz_app/data/models/subject/subject_model.dart'; import 'package:quiz_app/data/models/subject/subject_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/subject_service.dart'; import 'package:quiz_app/data/services/subject_service.dart';
import 'package:quiz_app/feature/navigation/controllers/navigation_controller.dart'; import 'package:quiz_app/feature/navigation/controllers/navigation_controller.dart';
class HomeController extends GetxController { class HomeController extends GetxController {
final UserController _userController; final UserController _userController = Get.find<UserController>();
final QuizService _quizService; final QuizService _quizService;
final SubjectService _subjectService; final SubjectService _subjectService;
final ConnectionService _connectionService;
HomeController( HomeController(
this._userController,
this._quizService, this._quizService,
this._subjectService, this._subjectService,
this._connectionService,
); );
RxInt timeStatus = 1.obs; RxInt timeStatus = 1.obs;
@ -43,10 +39,6 @@ class HomeController extends GetxController {
} }
void _getRecomendationQuiz() async { void _getRecomendationQuiz() async {
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return;
}
BaseResponseModel? response = await _quizService.recommendationQuiz(userId: _userController.userData!.id); BaseResponseModel? response = await _quizService.recommendationQuiz(userId: _userController.userData!.id);
if (response != null) { if (response != null) {
data.assignAll(response.data as List<QuizListingModel>); data.assignAll(response.data as List<QuizListingModel>);
@ -54,7 +46,6 @@ class HomeController extends GetxController {
} }
void loadSubjectData() async { void loadSubjectData() async {
if (!_connectionService.isCurrentlyConnected) return;
try { try {
final response = await _subjectService.getSubject(); final response = await _subjectService.getSubject();
subjects.assignAll(response.data!); subjects.assignAll(response.data!);

View File

@ -5,7 +5,6 @@ class UserGretingsComponent extends StatelessWidget {
final String userName; final String userName;
final String? userImage; final String? userImage;
final int greatingStatus; final int greatingStatus;
const UserGretingsComponent({ const UserGretingsComponent({
super.key, super.key,
required this.userName, required this.userName,

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/socket_service.dart'; import 'package:quiz_app/data/services/socket_service.dart';
import 'package:quiz_app/feature/join_room/controller/join_room_controller.dart'; import 'package:quiz_app/feature/join_room/controller/join_room_controller.dart';
@ -9,12 +8,6 @@ class JoinRoomBinding extends Bindings {
void dependencies() { void dependencies() {
Get.put(SocketService()); Get.put(SocketService());
Get.lazyPut( Get.lazyPut(() => JoinRoomController(Get.find<SocketService>(), Get.find<UserController>()));
() => JoinRoomController(
Get.find<SocketService>(),
Get.find<UserController>(),
Get.find<ConnectionService>(),
),
);
} }
} }

View File

@ -1,36 +1,23 @@
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/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/custom_floating_loading.dart'; import 'package:quiz_app/core/utils/custom_floating_loading.dart';
import 'package:quiz_app/core/utils/custom_notification.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/dto/waiting_room_dto.dart'; import 'package:quiz_app/data/dto/waiting_room_dto.dart';
import 'package:quiz_app/data/models/quiz/quiz_info_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_info_model.dart';
import 'package:quiz_app/data/models/session/session_info_model.dart'; import 'package:quiz_app/data/models/session/session_info_model.dart';
import 'package:quiz_app/data/models/session/session_response_model.dart'; import 'package:quiz_app/data/models/session/session_response_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/socket_service.dart'; import 'package:quiz_app/data/services/socket_service.dart';
class JoinRoomController extends GetxController { class JoinRoomController extends GetxController {
final SocketService _socketService; final SocketService _socketService;
final UserController _userController; final UserController _userController;
final ConnectionService _connectionService;
JoinRoomController( JoinRoomController(this._socketService, this._userController);
this._socketService,
this._userController,
this._connectionService,
);
final TextEditingController codeController = TextEditingController(); final TextEditingController codeController = TextEditingController();
RxBool isLoading = false.obs;
void joinRoom(BuildContext context) { void joinRoom() {
if (!_connectionService.isCurrentlyConnected) {
ConnectionNotification.noInternedConnection();
return;
}
final code = codeController.text.trim(); final code = codeController.text.trim();
if (code.isEmpty) { if (code.isEmpty) {
@ -42,15 +29,18 @@ class JoinRoomController extends GetxController {
); );
return; return;
} }
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoadingDialog(Get.context!);
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"); Get.snackbar(
CustomFloatingLoading.hideLoading(); "not found",
isLoading.value = false; "Ruangan tidak ditemukan",
backgroundColor: Get.theme.colorScheme.error.withValues(alpha: 0.9),
colorText: Colors.white,
);
CustomFloatingLoading.hideLoadingDialog(Get.context!);
}); });
_socketService.roomMessages.listen((data) { _socketService.roomMessages.listen((data) {
@ -59,8 +49,7 @@ class JoinRoomController extends GetxController {
final Map<String, dynamic> sessionInfoJson = dataPayload["session_info"]; final Map<String, dynamic> sessionInfoJson = dataPayload["session_info"];
final Map<String, dynamic> quizInfoJson = dataPayload["quiz_info"]; final Map<String, dynamic> quizInfoJson = dataPayload["quiz_info"];
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoadingDialog(Get.context!);
isLoading.value = false;
Get.toNamed( Get.toNamed(
AppRoutes.waitRoomPage, AppRoutes.waitRoomPage,
arguments: WaitingRoomDTO( arguments: WaitingRoomDTO(
@ -77,10 +66,6 @@ class JoinRoomController extends GetxController {
}); });
} }
void onGoBack() {
if (!isLoading.value) Get.back();
}
@override @override
void onClose() { void onClose() {
codeController.dispose(); codeController.dispose();

View File

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

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/feature/library/controller/library_controller.dart'; import 'package:quiz_app/feature/library/controller/library_controller.dart';
@ -10,10 +9,6 @@ class LibraryBinding extends Bindings {
if (!Get.isRegistered<QuizService>()) { if (!Get.isRegistered<QuizService>()) {
Get.lazyPut<QuizService>(() => QuizService()); Get.lazyPut<QuizService>(() => QuizService());
} }
Get.lazyPut<LibraryController>(() => LibraryController( Get.lazyPut<LibraryController>(() => LibraryController(Get.find<QuizService>(), Get.find<UserController>()));
Get.find<QuizService>(),
Get.find<UserController>(),
Get.find<ConnectionService>(),
));
} }
} }

View File

@ -1,26 +1,19 @@
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/core/helper/connection_check.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
class LibraryController extends GetxController { class LibraryController extends GetxController {
final QuizService _quizService;
final UserController _userController;
final ConnectionService _connectionService;
LibraryController(
this._quizService,
this._userController,
this._connectionService,
);
RxList<QuizListingModel> quizs = <QuizListingModel>[].obs; RxList<QuizListingModel> quizs = <QuizListingModel>[].obs;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxString emptyMessage = "".obs; RxString emptyMessage = "".obs;
final QuizService _quizService;
final UserController _userController;
LibraryController(this._quizService, this._userController);
int currentPage = 1; int currentPage = 1;
@override @override
@ -30,10 +23,6 @@ class LibraryController extends GetxController {
} }
void loadUserQuiz() async { void loadUserQuiz() async {
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return;
}
try { try {
isLoading.value = true; isLoading.value = true;
BaseResponseModel<List<QuizListingModel>>? response = await _quizService.userQuiz(_userController.userData!.id, currentPage); BaseResponseModel<List<QuizListingModel>>? response = await _quizService.userQuiz(_userController.userData!.id, currentPage);

View File

@ -1,11 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:quiz_app/app/const/text/text_style.dart';
import 'package:quiz_app/component/widget/container_skeleton_widget.dart'; import 'package:quiz_app/component/widget/container_skeleton_widget.dart';
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
import 'package:quiz_app/feature/library/controller/library_controller.dart'; import 'package:quiz_app/feature/library/controller/library_controller.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart';
class LibraryView extends GetView<LibraryController> { class LibraryView extends GetView<LibraryController> {
const LibraryView({super.key}); const LibraryView({super.key});
@ -13,7 +11,7 @@ class LibraryView extends GetView<LibraryController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background2, backgroundColor: const Color(0xFFF9FAFB),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
@ -22,12 +20,19 @@ class LibraryView extends GetView<LibraryController> {
children: [ children: [
Text( Text(
context.tr('library_title'), context.tr('library_title'),
style: AppTextStyles.title.copyWith(fontSize: 24), style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 24,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
context.tr('library_description'), context.tr('library_description'),
style: AppTextStyles.subtitle, style: const TextStyle(
color: Colors.grey,
fontSize: 14,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Expanded( Expanded(
@ -45,7 +50,7 @@ class LibraryView extends GetView<LibraryController> {
return Center( return Center(
child: Text( child: Text(
context.tr('no_quiz_available'), context.tr('no_quiz_available'),
style: AppTextStyles.caption, style: const TextStyle(color: Colors.grey, fontSize: 14),
), ),
); );
} }
@ -74,7 +79,7 @@ class LibraryView extends GetView<LibraryController> {
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.background, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@ -90,7 +95,7 @@ class LibraryView extends GetView<LibraryController> {
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primaryBlue, color: const Color(0xFF2563EB),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Icon(Icons.menu_book_rounded, color: Colors.white), child: const Icon(Icons.menu_book_rounded, color: Colors.white),
@ -102,42 +107,46 @@ class LibraryView extends GetView<LibraryController> {
children: [ children: [
Text( Text(
quiz.title, quiz.title,
style: AppTextStyles.body.copyWith( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
color: Colors.black,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
quiz.description, quiz.description,
style: AppTextStyles.caption, style: const TextStyle(
color: Colors.grey,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
const Icon(Icons.calendar_today_rounded, size: 14, color: AppColors.softGrayText), const Icon(Icons.calendar_today_rounded, size: 14, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
controller.formatDate(quiz.date), controller.formatDate(quiz.date),
style: AppTextStyles.dateTime, style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Icon(Icons.list, size: 14, color: AppColors.softGrayText), const Icon(Icons.list, size: 14, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
context.tr('quiz_count_named', namedArgs: {'total': quiz.totalQuiz.toString()}), context.tr('quiz_count_named', namedArgs: {'total': quiz.totalQuiz.toString()}),
style: AppTextStyles.dateTime, style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Icon(Icons.access_time, size: 14, color: AppColors.softGrayText), const Icon(Icons.access_time, size: 14, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
controller.formatDuration(quiz.duration), controller.formatDuration(quiz.duration),
style: AppTextStyles.dateTime, style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
], ],
), ),

View File

@ -3,7 +3,6 @@ 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/global_button.dart'; import 'package:quiz_app/component/global_button.dart';
import 'package:quiz_app/core/helper/connection_check.dart'; import 'package:quiz_app/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/custom_floating_loading.dart';
import 'package:quiz_app/core/utils/custom_notification.dart'; import 'package:quiz_app/core/utils/custom_notification.dart';
import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
@ -37,15 +36,13 @@ class LoginController extends GetxController {
final RxBool isPasswordHidden = true.obs; final RxBool isPasswordHidden = true.obs;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
late Worker _connectionWorker;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
emailController.addListener(validateFields); emailController.addListener(validateFields);
passwordController.addListener(validateFields); passwordController.addListener(validateFields);
_connectionWorker = ever(_connectionService.isConnected, (value) { ever(_connectionService.isConnected, (value) {
if (!value) { if (!value) {
ConnectionNotification.noInternedConnection(); ConnectionNotification.noInternedConnection();
} else { } else {
@ -90,7 +87,6 @@ class LoginController extends GetxController {
} }
try { try {
isLoading.value = true; isLoading.value = true;
CustomFloatingLoading.showLoading(Get.overlayContext!);
final LoginResponseModel response = await _authService.loginWithEmail( final LoginResponseModel response = await _authService.loginWithEmail(
LoginRequestModel(email: email, password: password), LoginRequestModel(email: email, password: password),
@ -101,14 +97,13 @@ class LoginController extends GetxController {
await _userStorageService.saveUser(userEntity); await _userStorageService.saveUser(userEntity);
_userController.setUserFromEntity(userEntity); _userController.setUserFromEntity(userEntity);
_userStorageService.isLogged = true; _userStorageService.isLogged = true;
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();
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");
} finally {
isLoading.value = false;
} }
} }
@ -118,23 +113,15 @@ class LoginController extends GetxController {
return; return;
} }
try { try {
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();
isLoading.value = false;
return; return;
} }
final idToken = await user.authentication.then((auth) => auth.idToken); final idToken = await user.authentication.then((auth) => auth.idToken);
if (idToken == null || idToken.isEmpty) { if (idToken == null || idToken.isEmpty) {
Get.snackbar("Kesalahan", "Tidak menerima ID Token dari Google"); Get.snackbar("Kesalahan", "Tidak menerima ID Token dari Google");
CustomFloatingLoading.hideLoading();
isLoading.value = false;
return; return;
} }
@ -144,25 +131,18 @@ class LoginController extends GetxController {
await _userStorageService.saveUser(userEntity); await _userStorageService.saveUser(userEntity);
_userController.setUserFromEntity(userEntity); _userController.setUserFromEntity(userEntity);
_userStorageService.isLogged = true; _userStorageService.isLogged = true;
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) {
logC.i("user data ${response.toJson()}"); logC.i("user id : ${response.id}");
return UserEntity( return UserEntity(
id: response.id ?? '', id: response.id ?? '',
name: response.name, name: response.name,
@ -174,10 +154,4 @@ class LoginController extends GetxController {
phone: response.phone, phone: response.phone,
); );
} }
@override
void onClose() {
_connectionWorker.dispose();
super.onClose();
}
} }

View File

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

@ -1,11 +1,10 @@
// feature/navbar/binding/navbar_binding.dart // feature/navbar/binding/navbar_binding.dart
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/feature/navigation/controllers/navigation_controller.dart'; import 'package:quiz_app/feature/navigation/controllers/navigation_controller.dart';
class NavbarBinding extends Bindings { class NavbarBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<NavigationController>(() => NavigationController(Get.find<ConnectionService>())); Get.lazyPut<NavigationController>(() => NavigationController());
} }
} }

View File

@ -1,14 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/core/helper/connection_check.dart';
import 'package:quiz_app/data/services/connection_service.dart';
class NavigationController extends GetxController { class NavigationController extends GetxController {
RxInt selectedIndex = 0.obs; RxInt selectedIndex = 0.obs;
final ConnectionService _connectionService;
NavigationController(this._connectionService);
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -18,18 +12,6 @@ class NavigationController extends GetxController {
} }
} }
@override
void onReady() {
ever(_connectionService.isConnected, (value) {
if (!value) {
ConnectionNotification.noInternedConnection();
} else {
ConnectionNotification.internetConnected();
}
});
super.onReady();
}
void changePage(int page) { void changePage(int page) {
selectedIndex.value = page; selectedIndex.value = page;
} }

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/google_auth_service.dart'; import 'package:quiz_app/data/services/google_auth_service.dart';
import 'package:quiz_app/data/services/user_service.dart'; import 'package:quiz_app/data/services/user_service.dart';
import 'package:quiz_app/data/services/user_storage_service.dart'; import 'package:quiz_app/data/services/user_storage_service.dart';
@ -16,7 +15,6 @@ class ProfileBinding extends Bindings {
Get.find<UserStorageService>(), Get.find<UserStorageService>(),
Get.find<GoogleAuthService>(), Get.find<GoogleAuthService>(),
Get.find<UserService>(), Get.find<UserService>(),
Get.find<ConnectionService>(),
)); ));
} }
} }

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/user_service.dart'; import 'package:quiz_app/data/services/user_service.dart';
import 'package:quiz_app/data/services/user_storage_service.dart'; import 'package:quiz_app/data/services/user_storage_service.dart';
import 'package:quiz_app/feature/profile/controller/update_profile_controller.dart'; import 'package:quiz_app/feature/profile/controller/update_profile_controller.dart';
@ -13,7 +12,6 @@ class UpdateProfileBinding extends Bindings {
Get.find<UserService>(), Get.find<UserService>(),
Get.find<UserController>(), Get.find<UserController>(),
Get.find<UserStorageService>(), Get.find<UserStorageService>(),
Get.find<ConnectionService>(),
)); ));
} }
} }

View File

@ -3,12 +3,10 @@ import 'package:easy_localization/easy_localization.dart';
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/notification/pop_up_confirmation.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';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/user/user_stat_model.dart'; import 'package:quiz_app/data/models/user/user_stat_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/google_auth_service.dart'; import 'package:quiz_app/data/services/google_auth_service.dart';
import 'package:quiz_app/data/services/user_service.dart'; import 'package:quiz_app/data/services/user_service.dart';
import 'package:quiz_app/data/services/user_storage_service.dart'; import 'package:quiz_app/data/services/user_storage_service.dart';
@ -19,14 +17,12 @@ class ProfileController extends GetxController {
final UserStorageService _userStorageService; final UserStorageService _userStorageService;
final GoogleAuthService _googleAuthService; final GoogleAuthService _googleAuthService;
final UserService _userService; final UserService _userService;
final ConnectionService _connectionService;
ProfileController( ProfileController(
this._userController, this._userController,
this._userStorageService, this._userStorageService,
this._googleAuthService, this._googleAuthService,
this._userService, this._userService,
this._connectionService,
); );
// User basic info // User basic info
@ -77,9 +73,6 @@ class ProfileController extends GetxController {
} }
void loadUserStat() async { void loadUserStat() async {
if (!await _connectionService.isHaveConnection()) {
return;
}
try { try {
final result = await _userService.getUserStat(_userController.userData!.id); final result = await _userService.getUserStat(_userController.userData!.id);
if (result != null) { if (result != null) {
@ -90,34 +83,21 @@ class ProfileController extends GetxController {
} }
} }
void logout(BuildContext context) async { void logout() async {
final confirm = await AppDialog.showConfirmationDialog( try {
context, await _googleAuthService.signOut();
title: "Keluar dari akun?", await _userStorageService.clearUser();
message: "Apakah Anda yakin ingin logout dari akun ini?", _userController.clearUser();
confirmText: "Logout", _userStorageService.isLogged = false;
); Get.offAllNamed(AppRoutes.loginPage);
} catch (e, stackTrace) {
if (confirm == true) { logC.e("Google Sign-Out Error: $e", stackTrace: stackTrace);
try { Get.snackbar("Error", "Gagal logout dari Google");
await _googleAuthService.signOut();
await _userStorageService.clearUser();
_userController.clearUser();
_userStorageService.isLogged = false;
Get.offAllNamed(AppRoutes.loginPage);
} catch (e, stackTrace) {
logC.e("Google Sign-Out Error: $e", stackTrace: stackTrace);
Get.snackbar("Error", "Gagal logout dari Google");
}
} }
} }
void editProfile() async { void editProfile() {
final resultUpdate = await Get.toNamed(AppRoutes.updateProfilePage); Get.toNamed(AppRoutes.updateProfilePage);
if (resultUpdate == true) {
loadUserProfileData();
}
} }
void changeLanguage(BuildContext context, String languageCode, String countryCode) async { void changeLanguage(BuildContext context, String languageCode, String countryCode) async {

View File

@ -1,12 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/custom_floating_loading.dart'; import 'package:quiz_app/core/utils/custom_floating_loading.dart';
import 'package:quiz_app/core/utils/custom_notification.dart'; import 'package:quiz_app/core/utils/custom_notification.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/entity/user/user_entity.dart'; import 'package:quiz_app/data/entity/user/user_entity.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/user_service.dart'; import 'package:quiz_app/data/services/user_service.dart';
import 'package:quiz_app/data/services/user_storage_service.dart'; import 'package:quiz_app/data/services/user_storage_service.dart';
@ -14,13 +11,11 @@ class UpdateProfileController extends GetxController {
final UserController _userController; final UserController _userController;
final UserStorageService _userStorageService; final UserStorageService _userStorageService;
final UserService _userService; final UserService _userService;
final ConnectionService _connectionService;
UpdateProfileController( UpdateProfileController(
this._userService, this._userService,
this._userController, this._userController,
this._userStorageService, this._userStorageService,
this._connectionService,
); );
final nameController = TextEditingController(); final nameController = TextEditingController();
@ -29,8 +24,6 @@ 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',
@ -51,86 +44,65 @@ class UpdateProfileController extends GetxController {
final name = nameController.text.trim(); final name = nameController.text.trim();
final phone = phoneController.text.trim(); final phone = phoneController.text.trim();
final birthDate = birthDateController.text.trim(); final birthDate = birthDateController.text.trim();
print(birthDate);
if (name.isEmpty || phone.isEmpty || birthDate.isEmpty) { if (name.isEmpty || phone.isEmpty || birthDate.isEmpty) {
CustomNotification.error( Get.snackbar('Validation Error', 'All fields must be filled.', snackPosition: SnackPosition.TOP);
title: 'Validation Error',
message: 'All fields must be filled.',
);
return false; return false;
} }
if (!_isValidDateFormat(birthDate)) { if (!_isValidDateFormat(birthDate)) {
CustomNotification.error( Get.snackbar('Validation Error', 'birth date must valid.', snackPosition: SnackPosition.TOP);
title: 'Validation Error',
message: 'birth date must valid.',
);
return false; return false;
} }
return true; return true;
} }
Future<void> saveProfile() async { Future<void> saveProfile() async {
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return;
}
if (!_validateInputs()) return; if (!_validateInputs()) return;
try { CustomFloatingLoading.showLoadingDialog(Get.context!);
CustomFloatingLoading.showLoading(Get.overlayContext!);
isLoading.value = true;
final isSuccessUpdate = await _userService.updateProfileData(
_userController.userData!.id,
nameController.text.trim(),
birthDate: birthDateController.text.trim(),
phone: phoneController.text.trim(),
locale: selectedLocale.value,
);
if (isSuccessUpdate) { final isSuccessUpdate = await _userService.updateProfileData(
final response = await _userService.getUserData(_userController.userData!.id); _userController.userData!.id,
nameController.text.trim(),
birthDate: birthDateController.text.trim(),
phone: phoneController.text.trim(),
locale: selectedLocale.value,
);
if (response?.data != null) { if (isSuccessUpdate) {
final userNew = response!.data!; final response = await _userService.getUserData(_userController.userData!.id);
final newUser = UserEntity(
id: userNew.id,
email: userNew.email,
name: userNew.name,
birthDate: userNew.birthDate,
locale: userNew.locale,
picUrl: userNew.picUrl,
phone: userNew.phone,
createdAt: userNew.createdAt,
);
_userStorageService.saveUser(newUser); if (response?.data != null) {
_userController.userData = newUser; final userNew = response!.data!;
final newUser = UserEntity(
id: userNew.id,
email: userNew.email,
name: userNew.name,
birthDate: userNew.birthDate,
locale: userNew.locale,
picUrl: userNew.picUrl,
phone: userNew.phone,
);
_userController.email.value = userNew.email; _userStorageService.saveUser(newUser);
_userController.userName.value = userNew.name; _userController.userData = newUser;
_userController.userImage.value = userNew.picUrl;
} _userController.email.value = userNew.email;
_userController.userName.value = userNew.name;
_userController.userImage.value = userNew.picUrl;
} }
CustomFloatingLoading.hideLoading();
isLoading.value = false;
Get.back(result: true);
CustomNotification.success(title: "Success", message: "Profile updated successfully");
} catch (e) {
CustomNotification.success(title: "something wrong", message: "failed to update profile");
isLoading.value = false;
logC.e(e);
} }
Get.back();
CustomNotification.success(title: "Success", message: "Profile updated successfully");
CustomFloatingLoading.hideLoadingDialog(Get.context!);
} }
bool _isValidDateFormat(String date) { bool _isValidDateFormat(String date) {
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

@ -36,7 +36,7 @@ class ProfileView extends GetView<ProfileController> {
const SizedBox(height: 10), const SizedBox(height: 10),
_profileDetails(cardRadius: cardRadius), _profileDetails(cardRadius: cardRadius),
const SizedBox(height: 10), const SizedBox(height: 10),
_settingsSection(context, cardRadius: cardRadius), _settingsSection(cardRadius: cardRadius),
const SizedBox(height: 10), const SizedBox(height: 10),
_legalSection(cardRadius: cardRadius), _legalSection(cardRadius: cardRadius),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -161,7 +161,7 @@ class ProfileView extends GetView<ProfileController> {
), ),
); );
Widget _settingsSection(BuildContext context, {required BorderRadius cardRadius}) => Card( Widget _settingsSection({required BorderRadius cardRadius}) => Card(
color: Colors.white, color: Colors.white,
elevation: 1, elevation: 1,
shadowColor: AppColors.shadowPrimary, shadowColor: AppColors.shadowPrimary,
@ -177,7 +177,7 @@ class ProfileView extends GetView<ProfileController> {
const Divider(height: 1), const Divider(height: 1),
_settingsTile(Get.context!, icon: LucideIcons.languages, title: tr('change_language'), onTap: () => _showLanguageDialog(Get.context!)), _settingsTile(Get.context!, icon: LucideIcons.languages, title: tr('change_language'), onTap: () => _showLanguageDialog(Get.context!)),
_settingsTile(Get.context!, _settingsTile(Get.context!,
icon: LucideIcons.logOut, title: tr('logout'), iconColor: Colors.red, textColor: Colors.red, onTap: () => controller.logout(context)), icon: LucideIcons.logOut, title: tr('logout'), iconColor: Colors.red, textColor: Colors.red, onTap: controller.logout),
], ],
), ),
), ),

View File

@ -1,7 +1,5 @@
import 'package:easy_localization/easy_localization.dart';
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/const/colors/app_colors.dart';
import 'package:quiz_app/component/global_button.dart'; import 'package:quiz_app/component/global_button.dart';
import 'package:quiz_app/component/global_dropdown_field.dart'; import 'package:quiz_app/component/global_dropdown_field.dart';
import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/global_text_field.dart';
@ -11,62 +9,56 @@ 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 PopScope( return Scaffold(
canPop: false, appBar: AppBar(
onPopInvokedWithResult: (didPop, result) => controller.onGoBack(), title: Text('Update Profile'),
child: Scaffold( centerTitle: true,
backgroundColor: AppColors.background2, ),
appBar: AppBar( body: Padding(
backgroundColor: AppColors.background2, padding: const EdgeInsets.all(16.0),
title: Text('Update Profile'), child: ListView(
centerTitle: true, children: [
), LabelTextField(label: "Name"),
body: Padding( GlobalTextField(controller: controller.nameController),
padding: const EdgeInsets.all(16.0), SizedBox(height: 16),
child: ListView( LabelTextField(label: "Phone"),
children: [ GlobalTextField(
LabelTextField(label: "Name"), controller: controller.phoneController,
GlobalTextField(controller: controller.nameController), hintText: 'Enter your phone number',
SizedBox(height: 16), ),
LabelTextField(label: "Phone"), SizedBox(height: 16),
GlobalTextField( LabelTextField(label: "Birth Date"),
controller: controller.phoneController, GlobalTextField(
hintText: 'Enter your phone number', controller: controller.birthDateController,
), hintText: 'Enter your birth date',
SizedBox(height: 16), ),
LabelTextField(label: "Birth Date"), SizedBox(height: 16),
GlobalTextField( LabelTextField(label: "Locale"),
controller: controller.birthDateController, Obx(() => GlobalDropdownField<String>(
hintText: 'Enter your birth date', value: controller.selectedLocale.value,
), items: controller.localeMap.entries.map<DropdownMenuItem<String>>((entry) {
SizedBox(height: 16), return DropdownMenuItem<String>(
LabelTextField(label: "Locale"), value: entry.value,
Obx(() => GlobalDropdownField<String>( child: Text(entry.key), // Display country name
value: controller.selectedLocale.value, );
items: controller.localeMap.entries.map<DropdownMenuItem<String>>((entry) { }).toList(),
return DropdownMenuItem<String>( onChanged: (String? newValue) {
value: entry.value, if (newValue != null) {
child: Text(entry.key), // Display country name controller.selectedLocale.value = newValue;
); final parts = newValue.split('-');
}).toList(), if (parts.length == 2) {
onChanged: (String? newValue) { Get.updateLocale(Locale(parts[0], parts[1]));
if (newValue != null) { } else {
controller.selectedLocale.value = newValue; Get.updateLocale(Locale(newValue));
final parts = newValue.split('-');
if (parts.length == 2) {
Get.updateLocale(Locale(parts[0], parts[1]));
} else {
Get.updateLocale(Locale(newValue));
}
} }
}, }
)), },
SizedBox(height: 32), )),
Center( SizedBox(height: 32),
child: GlobalButton(text: tr("save_changes"), onPressed: controller.saveProfile), Center(
), child: GlobalButton(text: "save_changes", onPressed: controller.saveProfile),
], ),
), ],
), ),
), ),
); );

View File

@ -1,5 +1,4 @@
import "package:get/get.dart"; import "package:get/get.dart";
import "package:quiz_app/data/services/connection_service.dart";
import "package:quiz_app/data/services/quiz_service.dart"; import "package:quiz_app/data/services/quiz_service.dart";
import "package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart"; import "package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart";
@ -10,7 +9,6 @@ class QuizCreationBinding extends Bindings {
Get.lazyPut<QuizCreationController>( Get.lazyPut<QuizCreationController>(
() => QuizCreationController( () => QuizCreationController(
Get.find<QuizService>(), Get.find<QuizService>(),
Get.find<ConnectionService>(),
), ),
); );
} }

View File

@ -5,22 +5,16 @@ import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/component/notification/delete_confirmation.dart'; import 'package:quiz_app/component/notification/delete_confirmation.dart';
import 'package:quiz_app/component/notification/pop_up_confirmation.dart'; import 'package:quiz_app/component/notification/pop_up_confirmation.dart';
import 'package:quiz_app/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/custom_floating_loading.dart'; import 'package:quiz_app/core/utils/custom_floating_loading.dart';
import 'package:quiz_app/core/utils/custom_notification.dart';
import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart'; import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
class QuizCreationController extends GetxController { class QuizCreationController extends GetxController {
final QuizService _quizService; final QuizService _quizService;
final ConnectionService _connectionService;
QuizCreationController( QuizCreationController(this._quizService);
this._quizService,
this._connectionService,
);
final TextEditingController inputSentenceTC = TextEditingController(); final TextEditingController inputSentenceTC = TextEditingController();
final TextEditingController questionTC = TextEditingController(); final TextEditingController questionTC = TextEditingController();
@ -35,8 +29,6 @@ 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();
@ -201,7 +193,7 @@ class QuizCreationController extends GetxController {
void onBack(BuildContext context) { void onBack(BuildContext context) {
if (quizData.length <= 1) { if (quizData.length <= 1) {
Get.back(); Navigator.pop(context);
} else { } else {
AppDialog.showExitConfirmationDialog(context); AppDialog.showExitConfirmationDialog(context);
} }
@ -232,77 +224,49 @@ class QuizCreationController extends GetxController {
} }
void generateQuiz() async { void generateQuiz() async {
if (!await _connectionService.isHaveConnection()) { CustomFloatingLoading.showLoadingDialog(Get.context!);
ConnectionNotification.noInternedConnection();
return;
}
if (inputSentenceTC.text.trim().isEmpty) {
CustomNotification.error(title: "Gagal", message: "kalimat atau paragraph tidak boleh kosong");
return;
}
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 && response.data!.isNotEmpty) { if (response.data != null) {
// Check if we should remove the initial empty question final previousLength = quizData.length;
bool shouldRemoveInitial = quizData.length == 1 && quizData[0].question == null && quizData[0].answer == null;
if (shouldRemoveInitial) { if (previousLength == 1) quizData.removeAt(0);
quizData.clear();
}
// Add new questions for (final i in response.data!) {
for (final quizItem in response.data!) {
QuestionType type = QuestionType.fillTheBlank; QuestionType type = QuestionType.fillTheBlank;
if (quizItem.answer.toString().toLowerCase() == 'true' || quizItem.answer.toString().toLowerCase() == 'false') { if (i.answer.toString().toLowerCase() == 'true' || i.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: quizItem.qustion, question: i.qustion,
answer: quizItem.answer, answer: i.answer,
type: type, type: type,
)); ));
} }
// Set the selected index to the first newly added question if (response.data!.isNotEmpty) {
if (shouldRemoveInitial) { selectedQuizIndex.value = previousLength;
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.hideLoadingDialog(Get.context!);
isLoading.value = false;
isGenerate.value = false; isGenerate.value = false;
inputSentenceTC.text = "";
if (quizData.isNotEmpty && selectedQuizIndex.value >= quizData.length) { if (quizData.isNotEmpty && selectedQuizIndex.value == 0) {
selectedQuizIndex.value = 0; final data = quizData[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;
@ -310,8 +274,4 @@ class QuizCreationController extends GetxController {
} }
} }
} }
onGoBack(BuildContext context, bool didPop) {
if (!isLoading.value) onBack(context);
}
} }

View File

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

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/subject_service.dart'; import 'package:quiz_app/data/services/subject_service.dart';
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart'; import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
@ -14,7 +13,6 @@ class QuizPreviewBinding extends Bindings {
Get.find<QuizService>(), Get.find<QuizService>(),
Get.find<UserController>(), Get.find<UserController>(),
Get.find<SubjectService>(), Get.find<SubjectService>(),
Get.find<ConnectionService>(),
)); ));
} }
} }

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/enums/question_type.dart'; import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/custom_floating_loading.dart'; import 'package:quiz_app/core/utils/custom_floating_loading.dart';
import 'package:quiz_app/core/utils/custom_notification.dart'; import 'package:quiz_app/core/utils/custom_notification.dart';
import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/core/utils/logger.dart';
@ -11,7 +10,6 @@ import 'package:quiz_app/data/models/quiz/question_create_request.dart';
import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/question_listings_model.dart';
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart'; import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
import 'package:quiz_app/data/models/subject/subject_model.dart'; import 'package:quiz_app/data/models/subject/subject_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/subject_service.dart'; import 'package:quiz_app/data/services/subject_service.dart';
@ -22,13 +20,11 @@ class QuizPreviewController extends GetxController {
final QuizService _quizService; final QuizService _quizService;
final UserController _userController; final UserController _userController;
final SubjectService _subjectService; final SubjectService _subjectService;
final ConnectionService _connectionService;
QuizPreviewController( QuizPreviewController(
this._quizService, this._quizService,
this._userController, this._userController,
this._subjectService, this._subjectService,
this._connectionService,
); );
RxBool isPublic = false.obs; RxBool isPublic = false.obs;
@ -74,10 +70,6 @@ class QuizPreviewController extends GetxController {
Future<void> onSaveQuiz() async { Future<void> onSaveQuiz() async {
try { try {
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return;
}
if (isLoading.value) return; if (isLoading.value) return;
final title = titleController.text.trim(); final title = titleController.text.trim();
@ -96,12 +88,11 @@ class QuizPreviewController extends GetxController {
title: 'Error', title: 'Error',
message: 'Jumlah soal harus 10 atau lebih', message: 'Jumlah soal harus 10 atau lebih',
); );
return; return;
} }
isLoading.value = true; isLoading.value = true;
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoadingDialog(Get.context!);
final now = DateTime.now(); final now = DateTime.now();
final String formattedDate = "${now.day.toString().padLeft(2, '0')}-${now.month.toString().padLeft(2, '0')}-${now.year}"; final String formattedDate = "${now.day.toString().padLeft(2, '0')}-${now.month.toString().padLeft(2, '0')}-${now.year}";
@ -125,14 +116,13 @@ class QuizPreviewController extends GetxController {
message: 'Kuis berhasil disimpan!', message: 'Kuis berhasil disimpan!',
); );
CustomFloatingLoading.hideLoading();
Get.offAllNamed(AppRoutes.mainPage, arguments: 2); Get.offAllNamed(AppRoutes.mainPage, arguments: 2);
} }
} catch (e) { } catch (e) {
CustomFloatingLoading.hideLoading();
logC.e(e); logC.e(e);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
// CustomFloatingLoading.hideLoadingDialog(Get.context!);
} }
} }
@ -180,10 +170,6 @@ 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,7 +34,6 @@ class SubjectDropdownComponent extends StatelessWidget {
} }
} }
}, },
dropdownColor: Colors.white,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,

View File

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

View File

@ -95,7 +95,7 @@ class QuizResultView extends GetView<QuizResultController> {
final parsed = _parseAnswer(question, answer.selectedAnswer); final parsed = _parseAnswer(question, answer.selectedAnswer);
return QuizItemWAComponent( return QuizItemWAComponent(
index: index + 1, index: index,
isCorrect: answer.isCorrect, isCorrect: answer.isCorrect,
question: question.question, question: question.question,
targetAnswer: parsed.targetAnswer, targetAnswer: parsed.targetAnswer,

View File

@ -26,8 +26,6 @@ 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) {
@ -83,8 +81,7 @@ class RegisterController extends GetxController {
} }
try { try {
CustomFloatingLoading.showLoading(Get.overlayContext!); CustomFloatingLoading.showLoadingDialog(Get.context!);
isLoading.value = true;
await _authService.register( await _authService.register(
RegisterRequestModel( RegisterRequestModel(
email: email, email: email,
@ -96,12 +93,10 @@ class RegisterController extends GetxController {
); );
Get.back(); Get.back();
CustomFloatingLoading.hideLoading(); CustomFloatingLoading.hideLoadingDialog(Get.context!);
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.hideLoadingDialog(Get.context!);
isLoading.value = false;
String errorMessage = e.toString().replaceFirst("Exception: ", ""); String errorMessage = e.toString().replaceFirst("Exception: ", "");

View File

@ -1,6 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/session_service.dart'; import 'package:quiz_app/data/services/session_service.dart';
import 'package:quiz_app/data/services/socket_service.dart'; import 'package:quiz_app/data/services/socket_service.dart';
@ -17,7 +16,6 @@ class RoomMakerBinding extends Bindings {
Get.find<UserController>(), Get.find<UserController>(),
Get.find<SocketService>(), Get.find<SocketService>(),
Get.find<QuizService>(), Get.find<QuizService>(),
Get.find<ConnectionService>(),
)); ));
} }
} }

View File

@ -1,8 +1,6 @@
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/core/helper/connection_check.dart';
import 'package:quiz_app/core/utils/custom_notification.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/dto/waiting_room_dto.dart'; import 'package:quiz_app/data/dto/waiting_room_dto.dart';
import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/base/base_model.dart';
@ -11,7 +9,6 @@ import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
import 'package:quiz_app/data/models/session/session_info_model.dart'; import 'package:quiz_app/data/models/session/session_info_model.dart';
import 'package:quiz_app/data/models/session/session_request_model.dart'; import 'package:quiz_app/data/models/session/session_request_model.dart';
import 'package:quiz_app/data/models/session/session_response_model.dart'; import 'package:quiz_app/data/models/session/session_response_model.dart';
import 'package:quiz_app/data/services/connection_service.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/session_service.dart'; import 'package:quiz_app/data/services/session_service.dart';
import 'package:quiz_app/data/services/socket_service.dart'; import 'package:quiz_app/data/services/socket_service.dart';
@ -21,14 +18,12 @@ class RoomMakerController extends GetxController {
final UserController _userController; final UserController _userController;
final SocketService _socketService; final SocketService _socketService;
final QuizService _quizService; final QuizService _quizService;
final ConnectionService _connectionService;
RoomMakerController( RoomMakerController(
this._sessionService, this._sessionService,
this._userController, this._userController,
this._socketService, this._socketService,
this._quizService, this._quizService,
this._connectionService,
); );
final selectedQuiz = Rxn<QuizListingModel>(); final selectedQuiz = Rxn<QuizListingModel>();
@ -52,10 +47,6 @@ class RoomMakerController extends GetxController {
} }
Future<void> loadQuiz({bool reset = false}) async { Future<void> loadQuiz({bool reset = false}) async {
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return;
}
if (isLoading) return; if (isLoading) return;
isLoading = true; isLoading = true;
@ -101,21 +92,8 @@ class RoomMakerController extends GetxController {
} }
void onCreateRoom() async { void onCreateRoom() async {
if (nameTC.text.trim().isEmpty || maxPlayerTC.text.trim().isEmpty || selectedQuiz.value == null) { if (nameTC.text.trim().isEmpty || selectedQuiz.value == null) {
CustomNotification.error(title: "Gagal", message: "Nama room, maksimal pemain dan kuis harus dipilih."); Get.snackbar("Gagal", "Nama room dan kuis harus dipilih.");
return;
}
if (int.tryParse(maxPlayerTC.text) == null) {
CustomNotification.error(
title: "Input tidak valid",
message: "Jumlah pemain harus berupa angka tanpa karakter huruf atau simbol.",
);
return;
}
if (!await _connectionService.isHaveConnection()) {
ConnectionNotification.noInternedConnection();
return; return;
} }
@ -125,7 +103,6 @@ class RoomMakerController extends GetxController {
SessionRequestModel( SessionRequestModel(
quizId: quiz.quizId, quizId: quiz.quizId,
hostId: _userController.userData!.id, hostId: _userController.userData!.id,
roomName: nameTC.text,
limitParticipan: int.parse(maxPlayerTC.text), limitParticipan: int.parse(maxPlayerTC.text),
), ),
); );

View File

@ -588,46 +588,52 @@ class RoomMakerView extends GetView<RoomMakerController> {
} }
Widget _buildCreateRoomButton() { Widget _buildCreateRoomButton() {
return AnimatedContainer( return Obx(() {
duration: const Duration(milliseconds: 300), final canCreate = controller.selectedQuiz.value != null && controller.nameTC.text.isNotEmpty && controller.maxPlayerTC.text.isNotEmpty;
width: MediaQuery.of(Get.context!).size.width - 32,
height: 56, return AnimatedContainer(
child: Material( duration: const Duration(milliseconds: 300),
elevation: 8, width: MediaQuery.of(Get.context!).size.width - 32,
borderRadius: BorderRadius.circular(16), height: 56,
child: InkWell( child: Material(
elevation: canCreate ? 8 : 2,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: controller.onCreateRoom, child: InkWell(
child: Container( borderRadius: BorderRadius.circular(16),
decoration: BoxDecoration( onTap: canCreate ? controller.onCreateRoom : null,
gradient: LinearGradient( child: Container(
colors: [AppColors.primaryBlue, AppColors.primaryBlue.withValues(alpha: 0.8)], decoration: BoxDecoration(
gradient: canCreate
? LinearGradient(
colors: [AppColors.primaryBlue, AppColors.primaryBlue.withValues(alpha: 0.8)],
)
: null,
color: !canCreate ? Colors.grey[300] : null,
borderRadius: BorderRadius.circular(16),
), ),
color: Colors.grey[300], child: Row(
borderRadius: BorderRadius.circular(16), mainAxisAlignment: MainAxisAlignment.center,
), children: [
child: Row( Icon(
mainAxisAlignment: MainAxisAlignment.center, Icons.add_circle,
children: [ color: canCreate ? Colors.white : Colors.grey[500],
Icon( size: 24,
Icons.add_circle,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
Text(
"Buat Room Sekarang",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), const SizedBox(width: 12),
], Text(
"Buat Room Sekarang",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: canCreate ? Colors.white : Colors.grey[500],
),
),
],
),
), ),
), ),
), ),
), );
); });
} }
} }

View File

@ -16,7 +16,6 @@ class WaitingRoomController extends GetxController {
WaitingRoomController(this._socketService, this._userController); WaitingRoomController(this._socketService, this._userController);
final sessionCode = ''.obs; final sessionCode = ''.obs;
final roomName = "".obs;
String sessionId = ''; String sessionId = '';
final quizMeta = Rx<QuizInfo?>(null); final quizMeta = Rx<QuizInfo?>(null);
final joinedUsers = <UserModel>[].obs; final joinedUsers = <UserModel>[].obs;
@ -43,7 +42,6 @@ class WaitingRoomController extends GetxController {
sessionId = roomData!.sessionId; sessionId = roomData!.sessionId;
quizMeta.value = data.quizInfo; quizMeta.value = data.quizInfo;
roomName.value = data.sessionInfo.roomName;
joinedUsers.assignAll(data.sessionInfo.participants); joinedUsers.assignAll(data.sessionInfo.participants);
} }

View File

@ -1,8 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:quiz_app/app/const/text/string_extension.dart';
import 'package:quiz_app/app/const/text/text_style.dart';
import 'package:quiz_app/component/global_button.dart'; import 'package:quiz_app/component/global_button.dart';
import 'package:quiz_app/data/models/quiz/quiz_info_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_info_model.dart';
import 'package:quiz_app/data/models/user/user_model.dart'; import 'package:quiz_app/data/models/user/user_model.dart';
@ -14,9 +11,7 @@ class WaitingRoomView extends GetView<WaitingRoomController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(title: const Text("Waiting Room")),
title: Text(tr("waiting_room.title"), style: AppTextStyles.title),
),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Obx(() { child: Obx(() {
@ -27,39 +22,25 @@ class WaitingRoomView extends GetView<WaitingRoomController> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 20),
Center(
child: Obx(() => Text(
controller.roomName.value.toTitleCase(),
style: AppTextStyles.title,
)),
),
const SizedBox(height: 20),
_buildQuizMeta(quiz!), _buildQuizMeta(quiz!),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildSessionCode(context, session), _buildSessionCode(context, session),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( const Text("Peserta yang Bergabung:", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
tr("waiting_room.participants_joined"),
style: AppTextStyles.subtitle.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.darkText,
),
),
const SizedBox(height: 10), const SizedBox(height: 10),
Expanded(child: Obx(() => _buildUserList(users.toList()))), Expanded(child: Obx(() => _buildUserList(users.toList()))),
const SizedBox(height: 16), const SizedBox(height: 16),
controller.isAdmin.value if (controller.isAdmin.value)
? GlobalButton( GlobalButton(
text: tr("start_quiz"), text: "Mulai Kuis",
onPressed: controller.startQuiz, onPressed: controller.startQuiz,
) )
: GlobalButton( else
text: tr("waiting_room.leave_room"), GlobalButton(
onPressed: controller.leaveRoom, text: "Tinggalkan Ruangan",
baseColor: const Color.fromARGB(255, 204, 14, 0), onPressed: controller.leaveRoom,
) baseColor: const Color.fromARGB(255, 204, 14, 0),
)
], ],
); );
}), }),
@ -71,19 +52,18 @@ class WaitingRoomView extends GetView<WaitingRoomController> {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.accentBlue.withOpacity(0.1), color: AppColors.primaryBlue.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.primaryBlue), border: Border.all(color: AppColors.primaryBlue),
), ),
child: Row( child: Row(
children: [ children: [
Text(tr("waiting_room.session_code"), style: AppTextStyles.statValue), const Text("Session Code: ", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 4), SelectableText(code, style: const TextStyle(fontSize: 16)),
SelectableText(code, style: AppTextStyles.body.copyWith(fontSize: 16)),
const Spacer(), const Spacer(),
IconButton( IconButton(
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),
tooltip: tr("waiting_room.copy_code"), tooltip: 'Salin Kode',
onPressed: () => controller.copySessionCode(context), onPressed: () => controller.copySessionCode(context),
), ),
], ],
@ -92,6 +72,7 @@ class WaitingRoomView extends GetView<WaitingRoomController> {
} }
Widget _buildQuizMeta(QuizInfo quiz) { Widget _buildQuizMeta(QuizInfo quiz) {
// if (quiz == null) return const SizedBox.shrink();
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
width: double.infinity, width: double.infinity,
@ -103,12 +84,12 @@ class WaitingRoomView extends GetView<WaitingRoomController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(tr("waiting_room.quiz_info"), style: AppTextStyles.subtitle.copyWith(fontWeight: FontWeight.bold)), const Text("Informasi Kuis:", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 8),
Text("${tr("waiting_room.quiz_title")}: ${quiz.title}", style: AppTextStyles.body), Text("Judul: ${quiz.title}"),
Text("${tr("waiting_room.quiz_description")}: ${quiz.description}", style: AppTextStyles.body), Text("Deskripsi: ${quiz.description}"),
Text("${tr("waiting_room.quiz_total_question")}: ${quiz.totalQuiz}", style: AppTextStyles.body), Text("Jumlah Soal: ${quiz.totalQuiz}"),
Text("${tr("waiting_room.quiz_duration")}: ${quiz.limitDuration ~/ 60} min", style: AppTextStyles.body), Text("Durasi: ${quiz.limitDuration ~/ 60} menit"),
], ],
), ),
); );
@ -129,9 +110,9 @@ class WaitingRoomView extends GetView<WaitingRoomController> {
), ),
child: Row( child: Row(
children: [ children: [
CircleAvatar(child: Text(user.username[0].toUpperCase())), CircleAvatar(child: Text(user.username[0])),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(user.username, style: AppTextStyles.body.copyWith(fontSize: 16)), Text(user.username, style: const TextStyle(fontSize: 16)),
], ],
), ),
); );

View File

@ -29,8 +29,7 @@ void main() {
Locale('ms', 'MY'), Locale('ms', 'MY'),
], ],
path: 'assets/translations', path: 'assets/translations',
fallbackLocale: Locale('id', 'ID'), fallbackLocale: Locale('en', 'US'),
startLocale: Locale('id', 'ID'),
useOnlyLangCode: false, useOnlyLangCode: false,
child: MyApp(), child: MyApp(),
), ),