develop #1

Merged
akhdanre merged 104 commits from develop into main 2025-07-10 12:38:53 +07:00
9 changed files with 250 additions and 53 deletions
Showing only changes of commit effaa4cdd7 - Show all commits

View File

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

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/services/user_storage_service.dart';
class UserController extends GetxController {
@ -9,6 +10,7 @@ class UserController extends GetxController {
Rx<String> userName = "".obs;
Rx<String?> userImage = Rx<String?>(null);
Rx<String> email = "".obs;
String userId = "";
@override
void onInit() {
@ -22,7 +24,9 @@ class UserController extends GetxController {
userName.value = data.name;
userImage.value = data.picUrl;
email.value = data.email;
print("Loaded user: ${data.toJson()}");
userId = data.id ?? "";
logC.i("user data $userId");
logC.i("Loaded user: ${data.toJson()}");
}
}
}

View File

@ -0,0 +1,65 @@
class QuizCreateRequestModel {
final String title;
final String description;
final bool isPublic;
final String date;
final int totalQuiz;
final int limitDuration;
final String authorId;
final List<QuestionListing> questionListings;
QuizCreateRequestModel({
required this.title,
required this.description,
required this.isPublic,
required this.date,
required this.totalQuiz,
required this.limitDuration,
required this.authorId,
required this.questionListings,
});
Map<String, dynamic> toJson() {
return {
'title': title,
'description': description,
'is_public': isPublic,
'date': date,
'total_quiz': totalQuiz,
'limit_duration': limitDuration,
'author_id': authorId,
'question_listings': questionListings.map((e) => e.toJson()).toList(),
};
}
}
class QuestionListing {
final String question;
final String targetAnswer;
final int duration;
final String type;
final List<String>? options;
QuestionListing({
required this.question,
required this.targetAnswer,
required this.duration,
required this.type,
this.options,
});
Map<String, dynamic> toJson() {
final map = <String, dynamic>{
'question': question,
'target_answer': targetAnswer,
'duration': duration,
'type': type,
};
if (options != null && options!.isNotEmpty) {
map['options'] = options;
}
return map;
}
}

View File

@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
import 'package:quiz_app/core/utils/logger.dart';
class ApiClient extends GetxService {
late final Dio dio;
@ -15,7 +16,37 @@ class ApiClient extends GetxService {
},
));
dio.interceptors.add(LogInterceptor(responseBody: true));
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
logC.i('''
[REQUEST]
[${options.method}] ${options.uri}
Headers: ${options.headers}
Body: ${options.data}
''');
return handler.next(options);
},
onResponse: (response, handler) {
logC.i('''
[RESPONSE]
[${response.statusCode}] ${response.requestOptions.uri}
Data: ${response.data}
''');
return handler.next(response);
},
onError: (DioException e, handler) {
logC.e('''
[ERROR]
[${e.response?.statusCode}] ${e.requestOptions.uri}
Message: ${e.message}
Error Data: ${e.response?.data}
''');
return handler.next(e);
},
),
);
return this;
}
}

View File

@ -0,0 +1,32 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
import 'package:quiz_app/data/models/quiz/question_create_request.dart';
import 'package:quiz_app/data/providers/dio_client.dart';
class QuizService extends GetxService {
late final Dio _dio;
@override
void onInit() {
_dio = Get.find<ApiClient>().dio;
super.onInit();
}
Future<bool> createQuiz(QuizCreateRequestModel request) async {
try {
final response = await _dio.post(
APIEndpoint.quiz,
data: request.toJson(),
);
if (response.statusCode == 201) {
return true;
} else {
throw Exception("Quiz creation failed");
}
} catch (e) {
throw Exception("Quiz creation error: $e");
}
}
}

View File

@ -13,8 +13,7 @@ class ProfileView extends GetView<ProfileController> {
child: Padding(
padding: const EdgeInsets.all(20),
child: Obx(() {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
return ListView(
children: [
const SizedBox(height: 20),
_buildAvatar(),

View File

@ -1,9 +1,12 @@
import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
class QuizPreviewBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<QuizPreviewController>(() => QuizPreviewController());
Get.lazyPut<QuizService>(() => QuizService());
Get.lazyPut<QuizPreviewController>(() => QuizPreviewController(Get.find<QuizService>(), Get.find<UserController>()));
}
}

View File

@ -2,12 +2,24 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/quiz/question_create_request.dart';
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
import 'package:quiz_app/data/services/quiz_service.dart';
class QuizPreviewController extends GetxController {
final TextEditingController titleController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
final QuizService _quizService;
final UserController _userController;
QuizPreviewController(this._quizService, this._userController);
RxBool isPublic = false.obs;
late final List<QuestionData> data;
@override
@ -25,7 +37,7 @@ class QuizPreviewController extends GetxController {
}
}
void onSaveQuiz() {
Future<void> onSaveQuiz() async {
final title = titleController.text.trim();
final description = descriptionController.text.trim();
@ -34,7 +46,56 @@ class QuizPreviewController extends GetxController {
return;
}
Get.snackbar('Sukses', 'Kuis berhasil disimpan!');
try {
final now = DateTime.now();
final String formattedDate = "${now.day.toString().padLeft(2, '0')}-${now.month.toString().padLeft(2, '0')}-${now.year}";
final quizRequest = QuizCreateRequestModel(
title: title,
description: description,
isPublic: isPublic.value,
date: formattedDate,
totalQuiz: data.length,
limitDuration: data.length * 30,
authorId: _userController.userId,
questionListings: _mapQuestionsToListings(data),
);
final success = await _quizService.createQuiz(quizRequest);
if (success) {
Get.snackbar('Sukses', 'Kuis berhasil disimpan!');
Get.offAllNamed(AppRoutes.mainPage);
}
} catch (e) {
logC.e(e);
}
}
List<QuestionListing> _mapQuestionsToListings(List<QuestionData> questions) {
return questions.map((q) {
String typeString;
switch (q.type) {
case QuestionType.fillTheBlank:
typeString = 'fill_the_blank';
break;
case QuestionType.option:
typeString = 'option';
break;
case QuestionType.trueOrFalse:
typeString = 'true_false';
break;
default:
typeString = 'fill_the_blank';
}
return QuestionListing(
question: q.question ?? '',
targetAnswer: q.answer ?? '',
duration: 30,
type: typeString,
options: q.options?.map((o) => o.text).toList(),
);
}).toList();
}
Widget buildQuestionCard(QuestionData question) {

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.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_text_field.dart';
import 'package:quiz_app/component/label_text_field.dart';
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
class QuizPreviewPage extends GetView<QuizPreviewController> {
@ -31,16 +33,13 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextField(
label: 'Judul Kuis',
controller: controller.titleController,
),
LabelTextField(label: "Judul"),
GlobalTextField(controller: controller.titleController),
const SizedBox(height: 20),
_buildTextField(
label: 'Deskripsi Kuis',
controller: controller.descriptionController,
maxLines: 3,
),
LabelTextField(label: "Deskripsi Singkat"),
GlobalTextField(controller: controller.descriptionController),
const SizedBox(height: 20),
_buildPublicCheckbox(), // Ganti ke sini
const SizedBox(height: 30),
const Divider(thickness: 1.2, color: AppColors.borderLight),
const SizedBox(height: 20),
@ -58,43 +57,6 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
);
}
Widget _buildTextField({
required String label,
required TextEditingController controller,
int maxLines = 1,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.softGrayText)),
const SizedBox(height: 8),
TextField(
controller: controller,
maxLines: maxLines,
decoration: InputDecoration(
hintText: 'Masukkan $label',
hintStyle: const TextStyle(color: AppColors.softGrayText),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.borderLight),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.borderLight),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
),
),
),
],
);
}
Widget _buildQuestionContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -103,4 +65,42 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
}).toList(),
);
}
Widget _buildPublicCheckbox() {
return Obx(
() => GestureDetector(
onTap: () {
controller.isPublic.toggle();
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Checkbox(
value: controller.isPublic.value,
activeColor: AppColors.primaryBlue, // Pakai warna biru utama
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
side: const BorderSide(
color: AppColors.primaryBlue, // Pinggirannya juga biru
width: 2,
),
onChanged: (value) {
controller.isPublic.value = value ?? false;
},
),
const SizedBox(width: 8),
const Text(
"Buat Kuis Public",
style: TextStyle(
fontSize: 16,
color: AppColors.darkText,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
}