diff --git a/.gitignore b/.gitignore
index b6323af..69216e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,6 @@ app.*.map.json
/android/app/release
# FVM Version Cache
-.fvm/
\ No newline at end of file
+.fvm/
+
+*.env
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..5706166
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,25 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "quiz_app",
+ "request": "launch",
+ "type": "dart"
+ },
+ // {
+ // "name": "quiz_app (profile mode)",
+ // "request": "launch",
+ // "type": "dart",
+ // "flutterMode": "profile"
+ // },
+ // {
+ // "name": "quiz_app (release mode)",
+ // "request": "launch",
+ // "type": "dart",
+ // "flutterMode": "release"
+ // }
+ ]
+}
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 2648e2a..2cfd4a7 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -30,12 +30,35 @@ android {
versionName = flutter.versionName
}
- buildTypes {
+ signingConfigs {
+ debug {
+ keyAlias = "keyDebugQuiz"
+ keyPassword = "uppercase12"
+ storeFile = file("debugKeystore.jks")
+ storePassword = "uppercase12"
+
+ }
+
release {
+ keyAlias = "genso-prod"
+ keyPassword = "oukenzeumasio"
+ storeFile = file("my-release-key.jks")
+ storePassword = "oukenzeumasio"
+
+ }
+
+ }
+
+ buildTypes {
+ debug {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
+
+ release {
+ signingConfig = signingConfigs.release
+ }
}
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 9e55388..3d04507 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
+
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
-
-
+
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..50b360e
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ 730226042143-mv9dlpk9cesirgjh2o0f9hvsk0ks8r2f.apps.googleusercontent.com
+
\ No newline at end of file
diff --git a/assets/logo/google_logo.png b/assets/logo/google_logo.png
new file mode 100644
index 0000000..a4a9918
Binary files /dev/null and b/assets/logo/google_logo.png differ
diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
new file mode 100644
index 0000000..1d589f6
--- /dev/null
+++ b/assets/translations/en-US.json
@@ -0,0 +1,157 @@
+{
+ "greeting_time": {
+ "morning": "Good Morning",
+ "afternoon": "Good Afternoon",
+ "evening": "Good Evening",
+ "night": "Good Night"
+ },
+
+ "greeting_user": "Hello {user}",
+ "create_room": "Create Room",
+ "join_room": "Join Room",
+ "create_quiz": "Create Quiz",
+ "ready_new_challenge": "Ready for a new challenge?",
+ "search_or_select_category": "Search or select by category",
+ "search_for_quizzes": "Search for quizzes...",
+ "quiz_recommendation": "Recommended Quiz",
+ "log_in": "Log In",
+ "sign_in": "Sign In",
+ "email": "Email",
+ "enter_your_email": "Enter Your Email",
+ "password": "Password",
+ "enter_your_password": "Enter Your Password",
+ "or": "OR",
+ "register_title": "Register",
+ "full_name": "Full Name",
+ "birth_date": "Birth Date",
+ "phone_optional": "Phone Number (Optional)",
+ "verify_password": "Verify Password",
+ "register_button": "Register",
+ "nav_home": "Home",
+ "nav_search": "Search",
+ "nav_library": "Library",
+ "nav_history": "History",
+ "nav_profile": "Profile",
+ "quiz_popular": "Popular Quiz",
+ "see_all": "See All",
+
+ "library_title": "Quiz Library",
+ "library_description": "A collection of quiz questions created for study.",
+ "no_quiz_available": "No quizzes available yet.",
+ "quiz_count_label": "Quiz",
+ "quiz_count_named": "{total} Quiz",
+
+ "history_title": "Quiz History",
+ "history_subtitle": "Review the quizzes you've taken",
+ "no_history": "You don't have any quiz history yet",
+ "score_label": "Score: {correct}/{total}",
+ "duration_minutes": "{minute} minutes",
+
+ "edit_profile": "Edit Profile",
+ "logout": "Logout",
+ "total_quiz": "Total Quiz",
+ "avg_score": "Average Score",
+
+ "history_detail_title": "Quiz Detail",
+ "score": "Score",
+ "time_taken": "Time",
+ "duration_seconds": "{second}s",
+
+ "question_type_option": "Multiple Choice",
+ "question_type_fill": "Fill in the Blank",
+ "question_type_true_false": "True / False",
+ "question_type_unknown": "Unknown Type",
+
+ "enter_room_code": "Enter Room Code",
+ "room_code_hint": "AB123C",
+ "join_now": "Join Now",
+
+ "create_quiz_title": "Create Quiz",
+ "save_all": "Save All",
+ "mode_generate": "Generate",
+ "mode_manual": "Manual",
+
+ "quiz_play_title": "Answer Quiz",
+ "ready_in": "Ready in {second}",
+ "question_indicator": "Question {current} of {total}",
+ "yes": "Yes",
+ "no": "No",
+ "next": "Next",
+
+ "quiz_preview_title": "Preview Quiz",
+ "quiz_title_label": "Title",
+ "quiz_description_label": "Short Description",
+ "quiz_subject_label": "Subject",
+ "make_quiz_public": "Make Quiz Public",
+ "save_quiz": "Save Quiz",
+
+ "select_language": "Select Language",
+ "change_language": "Change Language",
+
+ "auto_generate_quiz": "Auto Generate Quiz",
+ "ready_to_compete": "Ready to Compete?",
+ "enter_code_to_join": "Enter the quiz code and show your skills!",
+ "join_quiz_now": "Join Quiz Now",
+
+ "total_solve": "Total Solved",
+ "personal_info": "Personal Information",
+ "phone": "Phone Number",
+ "location": "Location",
+ "joined": "Joined",
+ "education": "Education",
+ "not_set": "Not Set",
+ "not_available": "Not Available",
+ "settings": "Settings",
+ "legal_and_support": "Legal & Support",
+ "privacy_policy": "Privacy Policy",
+ "terms_of_service": "Terms of Service",
+ "help_center": "Help Center",
+ "contact_us": "Contact Us",
+ "about_app": "About App",
+ "version": "Version",
+ "close": "Close",
+
+ "your_answer": "Your answer: {answer}",
+ "correct": "Correct",
+ "correct_answer": "Correct answer: {answer}",
+ "not_answered": "Not Answered",
+ "seconds_suffix": "s",
+ "quiz_type_option": "Multiple Choice",
+ "quiz_type_true_false": "True or False",
+ "quiz_type_fill_the_blank": "Fill in the Blank",
+
+ "quiz_detail_title": "Quiz Detail",
+ "question_label": "Question",
+ "duration_label": "Duration",
+ "minutes_suffix": "minutes",
+ "start_quiz": "Start Quiz",
+
+ "duration": {
+ "second": "{} second",
+ "minute": "{} minute",
+ "hour": "{} hour"
+ },
+ "duration_suffix": {
+ "second": "{} s",
+ "minute": "{} m",
+ "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"
+}
diff --git a/assets/translations/id-ID.json b/assets/translations/id-ID.json
new file mode 100644
index 0000000..2413acc
--- /dev/null
+++ b/assets/translations/id-ID.json
@@ -0,0 +1,140 @@
+{
+ "greeting_time": {
+ "morning": "Selamat Pagi",
+ "afternoon": "Selamat Siang",
+ "evening": "Selamat Sore",
+ "night": "Selamat Malam"
+ },
+ "greeting_user": "Halo {user}",
+ "create_room": "Buat Ruangan",
+ "join_room": "Gabung Ruang",
+ "create_quiz": "Buat Kuis",
+ "ready_new_challenge": "Siap untuk tantangan baru?",
+ "search_or_select_category": "Cari atau pilih berdasarkan kategori",
+ "search_for_quizzes": "Cari kuis...",
+ "quiz_recommendation": "Rekomendasi Kuis",
+ "log_in": "Masuk",
+ "sign_in": "Masuk",
+ "email": "Email",
+ "enter_your_email": "Masukkan Email Kamu",
+ "password": "Kata Sandi",
+ "enter_your_password": "Masukkan Kata Sandi Kamu",
+ "or": "ATAU",
+ "register_title": "Daftar",
+ "full_name": "Nama Lengkap",
+ "birth_date": "Tanggal Lahir",
+ "phone_optional": "Nomor Telepon (Opsional)",
+ "verify_password": "Verifikasi Kata Sandi",
+ "register_button": "Daftar",
+ "nav_home": "Beranda",
+ "nav_search": "Cari",
+ "nav_library": "Pustaka",
+ "nav_history": "Riwayat",
+ "nav_profile": "Profil",
+ "quiz_popular": "Kuis Populer",
+ "see_all": "Lihat Semua",
+ "library_title": "Pustaka Kuis",
+ "library_description": "Kumpulan pertanyaan kuis untuk belajar.",
+ "no_quiz_available": "Belum ada kuis yang tersedia.",
+ "quiz_count_label": "Kuis",
+ "quiz_count_named": "{total} Kuis",
+ "history_title": "Riwayat Kuis",
+ "history_subtitle": "Tinjau kuis yang telah kamu kerjakan",
+ "no_history": "Kamu belum memiliki riwayat kuis",
+ "score_label": "Skor: {correct}/{total}",
+ "duration_minutes": "{minute} menit",
+ "edit_profile": "Edit Profil",
+ "logout": "Keluar",
+ "total_quiz": "Total Kuis",
+ "avg_score": "Skor Rata-rata",
+ "history_detail_title": "Detail Kuis",
+ "score": "Skor",
+ "time_taken": "Waktu",
+ "duration_seconds": "{second} detik",
+ "question_type_option": "Pilihan Ganda",
+ "question_type_fill": "Isian Kosong",
+ "question_type_true_false": "Benar / Salah",
+ "question_type_unknown": "Tipe Tidak Dikenal",
+ "enter_room_code": "Masukkan Kode Ruangan",
+ "room_code_hint": "AB123C",
+ "join_now": "Gabung Sekarang",
+ "create_quiz_title": "Buat Kuis",
+ "save_all": "Simpan Semua",
+ "mode_generate": "Otomatis",
+ "mode_manual": "Manual",
+ "quiz_play_title": "Kerjakan Kuis",
+ "ready_in": "Siap dalam {second}",
+ "question_indicator": "Pertanyaan {current} dari {total}",
+ "yes": "Ya",
+ "no": "Tidak",
+ "next": "Berikutnya",
+ "quiz_preview_title": "Pratinjau Kuis",
+ "quiz_title_label": "Judul",
+ "quiz_description_label": "Deskripsi Singkat",
+ "quiz_subject_label": "Mata Pelajaran",
+ "make_quiz_public": "Jadikan Kuis Publik",
+ "save_quiz": "Simpan Kuis",
+ "select_language": "Pilih Bahasa",
+ "change_language": "Ganti Bahasa",
+ "auto_generate_quiz": "Buat Kuis Otomatis",
+ "ready_to_compete": "Siap untuk Bertanding?",
+ "enter_code_to_join": "Masukkan kode kuis dan tunjukkan kemampuanmu!",
+ "join_quiz_now": "Gabung Kuis Sekarang",
+ "total_solve": "Total Diselesaikan",
+ "personal_info": "Informasi Pribadi",
+ "phone": "Nomor Telepon",
+ "location": "Lokasi",
+ "joined": "Bergabung",
+ "education": "Pendidikan",
+ "not_set": "Belum Diatur",
+ "not_available": "Tidak Tersedia",
+ "settings": "Pengaturan",
+ "legal_and_support": "Legal & Bantuan",
+ "privacy_policy": "Kebijakan Privasi",
+ "terms_of_service": "Syarat dan Ketentuan",
+ "help_center": "Pusat Bantuan",
+ "contact_us": "Hubungi Kami",
+ "about_app": "Tentang Aplikasi",
+ "version": "Versi",
+ "close": "Tutup",
+ "your_answer": "Jawabanmu: {answer}",
+ "correct": "Benar",
+ "correct_answer": "Jawaban benar: {answer}",
+ "not_answered": "Tidak Menjawab",
+ "seconds_suffix": "d",
+ "quiz_type_option": "Pilihan Ganda",
+ "quiz_type_true_false": "Benar atau Salah",
+ "quiz_type_fill_the_blank": "Isian Kosong",
+ "quiz_detail_title": "Detail Kuis",
+ "question_label": "Pertanyaan",
+ "duration_label": "Durasi",
+ "minutes_suffix": "menit",
+ "start_quiz": "Start Quiz",
+ "duration": {
+ "second": "{} detik",
+ "minute": "{} menit",
+ "hour": "{} jam"
+ },
+ "duration_suffix": {
+ "second": "{} d",
+ "minute": "{} m",
+ "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"
+
+}
diff --git a/assets/translations/ms-MY.json b/assets/translations/ms-MY.json
new file mode 100644
index 0000000..31bbb63
--- /dev/null
+++ b/assets/translations/ms-MY.json
@@ -0,0 +1,141 @@
+{
+ "greeting_time": {
+ "morning": "Selamat Pagi",
+ "afternoon": "Selamat Tengah Hari",
+ "evening": "Selamat Petang",
+ "night": "Selamat Malam"
+ },
+ "greeting_user": "Hai {user}",
+ "create_room": "Cipta Bilik",
+ "join_room": "Sertai Bilik",
+ "create_quiz": "Cipta Kuiz",
+ "ready_new_challenge": "Sedia untuk cabaran baru?",
+ "search_or_select_category": "Cari atau pilih ikut kategori",
+ "search_for_quizzes": "Cari kuiz...",
+ "quiz_recommendation": "Kuiz Disyorkan",
+ "log_in": "Log Masuk",
+ "sign_in": "Log Masuk",
+ "email": "Emel",
+ "enter_your_email": "Masukkan Emel Kamu",
+ "password": "Kata Laluan",
+ "enter_your_password": "Masukkan Kata Laluan Kamu",
+ "or": "ATAU",
+ "register_title": "Daftar",
+ "full_name": "Nama Penuh",
+ "birth_date": "Tarikh Lahir",
+ "phone_optional": "Nombor Telefon (Opsyenal)",
+ "verify_password": "Sahkan Kata Laluan",
+ "register_button": "Daftar",
+ "nav_home": "Laman Utama",
+ "nav_search": "Cari",
+ "nav_library": "Perpustakaan",
+ "nav_history": "Sejarah",
+ "nav_profile": "Profil",
+ "quiz_popular": "Kuiz Popular",
+ "see_all": "Lihat Semua",
+ "library_title": "Perpustakaan Kuiz",
+ "library_description": "Koleksi soalan kuiz untuk belajar.",
+ "no_quiz_available": "Tiada kuiz tersedia buat masa ini.",
+ "quiz_count_label": "Kuiz",
+ "quiz_count_named": "{total} Kuiz",
+ "history_title": "Sejarah Kuiz",
+ "history_subtitle": "Semak semula kuiz yang kamu dah jawab",
+ "no_history": "Kamu belum ada sejarah kuiz",
+ "score_label": "Skor: {correct}/{total}",
+ "duration_minutes": "{minute} minit",
+ "edit_profile": "Edit Profil",
+ "logout": "Log Keluar",
+ "total_quiz": "Jumlah Kuiz",
+ "avg_score": "Skor Purata",
+ "history_detail_title": "Butiran Kuiz",
+ "score": "Skor",
+ "time_taken": "Masa Diambil",
+ "duration_seconds": "{second} saat",
+ "question_type_option": "Pilihan Jawapan",
+ "question_type_fill": "Isian Kosong",
+ "question_type_true_false": "Betul / Salah",
+ "question_type_unknown": "Jenis Tidak Diketahui",
+ "enter_room_code": "Masukkan Kod Bilik",
+ "room_code_hint": "AB123C",
+ "join_now": "Sertai Sekarang",
+ "create_quiz_title": "Cipta Kuiz",
+ "save_all": "Simpan Semua",
+ "mode_generate": "Auto",
+ "mode_manual": "Manual",
+ "quiz_play_title": "Jawab Kuiz",
+ "ready_in": "Sedia dalam {second}",
+ "question_indicator": "Soalan {current} daripada {total}",
+ "yes": "Ya",
+ "no": "Tidak",
+ "next": "Seterusnya",
+ "quiz_preview_title": "Pratonton Kuiz",
+ "quiz_title_label": "Tajuk",
+ "quiz_description_label": "Deskripsi Ringkas",
+ "quiz_subject_label": "Subjek",
+ "make_quiz_public": "Jadikan Kuiz Umum",
+ "save_quiz": "Simpan Kuiz",
+ "select_language": "Pilih Bahasa",
+ "change_language": "Tukar Bahasa",
+ "auto_generate_quiz": "Cipta Kuiz Automatik",
+ "ready_to_compete": "Sedia untuk Bertanding?",
+ "enter_code_to_join": "Masukkan kod kuiz dan tunjukkan kebolehan kamu!",
+ "join_quiz_now": "Sertai Kuiz Sekarang",
+ "total_solve": "Jumlah Diselesaikan",
+ "personal_info": "Maklumat Peribadi",
+ "phone": "Nombor Telefon",
+ "location": "Lokasi",
+ "joined": "Mendaftar",
+ "education": "Pendidikan",
+ "not_set": "Belum Ditentukan",
+ "not_available": "Tidak Tersedia",
+ "settings": "Tetapan",
+ "legal_and_support": "Perundangan & Sokongan",
+ "privacy_policy": "Dasar Privasi",
+ "terms_of_service": "Terma Perkhidmatan",
+ "help_center": "Pusat Bantuan",
+ "contact_us": "Hubungi Kami",
+ "about_app": "Tentang Aplikasi",
+ "version": "Versi",
+ "close": "Tutup",
+ "your_answer": "Jawapan kamu: {answer}",
+ "correct": "Betul",
+ "correct_answer": "Jawapan yang betul: {answer}",
+ "not_answered": "Belum Dijawab",
+ "seconds_suffix": "saat",
+ "quiz_type_option": "Pilihan Jawapan",
+ "quiz_type_true_false": "Betul atau Salah",
+ "quiz_type_fill_the_blank": "Isian Kosong",
+ "quiz_detail_title": "Butiran Kuiz",
+ "question_label": "Soalan",
+ "duration_label": "Durasi",
+ "minutes_suffix": "minit",
+ "start_quiz": "Mula Kuiz",
+
+ "duration": {
+ "second": "{} saat",
+ "minute": "{} minit",
+ "hour": "{} jam"
+ },
+
+ "duration_suffix": {
+ "second": "{} s",
+ "minute": "{} m",
+ "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"
+}
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/lib/app/app.dart b/lib/app/app.dart
new file mode 100644
index 0000000..0714483
--- /dev/null
+++ b/lib/app/app.dart
@@ -0,0 +1,24 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:quiz_app/app/bindings/initial_bindings.dart';
+import 'package:quiz_app/app/routes/app_pages.dart';
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return GetMaterialApp(
+ title: 'Quiz App',
+ locale: Get.locale ?? context.locale,
+ fallbackLocale: const Locale('id', 'ID'),
+ localizationsDelegates: context.localizationDelegates,
+ supportedLocales: context.supportedLocales,
+ initialBinding: InitialBindings(),
+ initialRoute: AppRoutes.splashScreen,
+ getPages: AppPages.routes,
+ debugShowCheckedModeBanner: false,
+ );
+ }
+}
diff --git a/lib/app/bindings/initial_bindings.dart b/lib/app/bindings/initial_bindings.dart
new file mode 100644
index 0000000..cc212a4
--- /dev/null
+++ b/lib/app/bindings/initial_bindings.dart
@@ -0,0 +1,15 @@
+import 'package:get/get.dart';
+import 'package:quiz_app/data/controllers/user_controller.dart';
+import 'package:quiz_app/data/providers/dio_client.dart';
+import 'package:quiz_app/data/services/connection_service.dart';
+import 'package:quiz_app/data/services/user_storage_service.dart';
+
+class InitialBindings extends Bindings {
+ @override
+ void dependencies() {
+ Get.put(UserStorageService());
+ Get.put(ConnectionService());
+ Get.putAsync(() => ApiClient().init());
+ Get.put(UserController(Get.find()));
+ }
+}
diff --git a/lib/app/const/colors/app_colors.dart b/lib/app/const/colors/app_colors.dart
new file mode 100644
index 0000000..398a4d3
--- /dev/null
+++ b/lib/app/const/colors/app_colors.dart
@@ -0,0 +1,20 @@
+import 'dart:ui';
+
+class AppColors {
+ static const Color primaryBlue = Color(0xFF0052CC);
+ static const Color darkText = Color(0xFF172B4D);
+ static const Color softGrayText = Color(0xFF6B778C);
+ static const Color background = Color(0xFFFAFBFC);
+ static const Color background2 = Color(0xFFF9FAFB);
+
+ static const Color borderLight = Color(0xFFE1E4E8);
+ static const Color accentBlue = Color(0xFFD6E4FF);
+ static const Color shadowPrimary = Color(0x330052CC);
+ static const Color disabledBackground = Color(0xFFE0E0E0);
+ static const Color disabledText = Color(0xFF9E9E9E);
+
+ static const Color scoreExcellent = Color(0xFF36B37E);
+ static const Color scoreGood = Color(0xFF00B8D9);
+ static const Color scoreAverage = Color(0xFFFF991F);
+ static const Color scorePoor = Color(0xFFFF5630);
+}
diff --git a/lib/app/const/enums/listing_type.dart b/lib/app/const/enums/listing_type.dart
new file mode 100644
index 0000000..ac76db8
--- /dev/null
+++ b/lib/app/const/enums/listing_type.dart
@@ -0,0 +1 @@
+enum ListingType { recomendation, populer, subject }
diff --git a/lib/app/const/enums/question_type.dart b/lib/app/const/enums/question_type.dart
new file mode 100644
index 0000000..d61ca6f
--- /dev/null
+++ b/lib/app/const/enums/question_type.dart
@@ -0,0 +1 @@
+enum QuestionType { fillTheBlank, option, trueOrFalse }
diff --git a/lib/app/const/text/string_extension.dart b/lib/app/const/text/string_extension.dart
new file mode 100644
index 0000000..060a04f
--- /dev/null
+++ b/lib/app/const/text/string_extension.dart
@@ -0,0 +1,5 @@
+extension StringCasingExtension on String {
+ String toTitleCase() {
+ return split(' ').map((word) => word.isNotEmpty ? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}' : '').join(' ');
+ }
+}
diff --git a/lib/app/const/text/text_style.dart b/lib/app/const/text/text_style.dart
new file mode 100644
index 0000000..f6697cc
--- /dev/null
+++ b/lib/app/const/text/text_style.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:quiz_app/app/const/colors/app_colors.dart';
+
+class AppTextStyles {
+ /// Title: strong and modern using Roboto
+ static final TextStyle title = GoogleFonts.roboto(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: AppColors.darkText,
+ );
+
+ /// Subtitle: clean and readable using Inter
+ static final TextStyle subtitle = GoogleFonts.inter(
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: AppColors.softGrayText,
+ );
+
+ /// Body: neutral and easy-to-read using Inter
+ static final TextStyle body = GoogleFonts.inter(
+ fontSize: 14,
+ fontWeight: FontWeight.w400,
+ color: AppColors.darkText,
+ );
+
+ /// Caption: friendly and soft using Nunito
+ static final TextStyle caption = GoogleFonts.nunito(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: AppColors.softGrayText,
+ );
+
+ /// Stat value: bold and standout using Poppins
+ static final TextStyle statValue = GoogleFonts.poppins(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ color: AppColors.darkText,
+ );
+
+ /// Option text: clean and consistent using Inter
+ static final TextStyle optionText = GoogleFonts.inter(
+ fontSize: 14,
+ fontWeight: FontWeight.w400,
+ color: AppColors.darkText,
+ );
+
+ /// DateTime: subtle and elegant using Nunito Italic
+ static final TextStyle dateTime = GoogleFonts.nunito(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ fontStyle: FontStyle.italic,
+ color: AppColors.softGrayText,
+ );
+}
diff --git a/lib/app/middleware/auth_middleware.dart b/lib/app/middleware/auth_middleware.dart
new file mode 100644
index 0000000..2dcea74
--- /dev/null
+++ b/lib/app/middleware/auth_middleware.dart
@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:quiz_app/app/routes/app_pages.dart';
+import 'package:quiz_app/data/services/user_storage_service.dart';
+
+class AuthMiddleware extends GetMiddleware {
+ @override
+ RouteSettings? redirect(String? route) {
+ final UserStorageService _storageService = Get.find();
+ if (!_storageService.isLogged) {
+ return const RouteSettings(name: AppRoutes.loginPage);
+ }
+ return null;
+ }
+}
diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart
new file mode 100644
index 0000000..81fcddc
--- /dev/null
+++ b/lib/app/routes/app_pages.dart
@@ -0,0 +1,160 @@
+import 'package:get/get_navigation/src/routes/get_route.dart';
+import 'package:quiz_app/app/middleware/auth_middleware.dart';
+import 'package:quiz_app/feature/admin_result_page/bindings/admin_result_binding.dart';
+import 'package:quiz_app/feature/admin_result_page/bindings/detail_participant_result_binding.dart';
+import 'package:quiz_app/feature/admin_result_page/view/admin_result_page.dart';
+import 'package:quiz_app/feature/admin_result_page/view/detail_participant_result_page.dart';
+import 'package:quiz_app/feature/history/binding/detail_history_binding.dart';
+import 'package:quiz_app/feature/history/binding/history_binding.dart';
+import 'package:quiz_app/feature/history/view/detail_history_view.dart';
+import 'package:quiz_app/feature/home/binding/home_binding.dart';
+import 'package:quiz_app/feature/home/view/home_page.dart';
+import 'package:quiz_app/feature/detail_quiz/binding/detail_quiz_binding.dart';
+import 'package:quiz_app/feature/join_room/binding/join_room_binding.dart';
+import 'package:quiz_app/feature/join_room/view/join_room_view.dart';
+import 'package:quiz_app/feature/library/binding/library_binding.dart';
+import 'package:quiz_app/feature/detail_quiz/view/detail_quix_view.dart';
+import 'package:quiz_app/feature/listing_quiz/binding/listing_quiz_binding.dart';
+import 'package:quiz_app/feature/listing_quiz/view/listing_quiz_view.dart';
+import 'package:quiz_app/feature/login/bindings/login_binding.dart';
+import 'package:quiz_app/feature/login/view/login_page.dart';
+import 'package:quiz_app/feature/monitor_quiz/binding/monitor_quiz_binding.dart';
+import 'package:quiz_app/feature/monitor_quiz/view/monitor_quiz_view.dart';
+import 'package:quiz_app/feature/navigation/bindings/navigation_binding.dart';
+import 'package:quiz_app/feature/navigation/views/navbar_view.dart';
+import 'package:quiz_app/feature/play_quiz_multiplayer/binding/play_quiz_multiplayer_binding.dart';
+import 'package:quiz_app/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart';
+import 'package:quiz_app/feature/profile/binding/profile_binding.dart';
+import 'package:quiz_app/feature/profile/binding/update_profile_binding.dart';
+import 'package:quiz_app/feature/profile/view/update_profile_view.dart';
+import 'package:quiz_app/feature/quiz_creation/binding/quiz_creation_binding.dart';
+import 'package:quiz_app/feature/quiz_creation/view/quiz_creation_view.dart';
+import 'package:quiz_app/feature/quiz_play/binding/quiz_play_binding.dart';
+import 'package:quiz_app/feature/quiz_play/view/quiz_play_view.dart';
+import 'package:quiz_app/feature/quiz_preview/binding/quiz_preview_binding.dart';
+import 'package:quiz_app/feature/quiz_preview/view/quiz_preview.dart';
+import 'package:quiz_app/feature/quiz_result/binding/quiz_result_binding.dart';
+import 'package:quiz_app/feature/quiz_result/view/quiz_result_view.dart';
+import 'package:quiz_app/feature/register/binding/register_binding.dart';
+import 'package:quiz_app/feature/register/view/register_page.dart';
+import 'package:quiz_app/feature/room_maker/binding/room_maker_binding.dart';
+import 'package:quiz_app/feature/room_maker/view/room_maker_view.dart';
+import 'package:quiz_app/feature/search/binding/search_binding.dart';
+import 'package:quiz_app/feature/splash_screen/presentation/splash_screen_page.dart';
+import 'package:quiz_app/feature/waiting_room/binding/waiting_room_binding.dart';
+import 'package:quiz_app/feature/waiting_room/view/waiting_room_view.dart';
+
+part 'app_routes.dart';
+
+class AppPages {
+ static List> routes = [
+ GetPage(
+ name: AppRoutes.splashScreen,
+ page: () => SplashScreenView(),
+ ),
+ GetPage(
+ name: AppRoutes.loginPage,
+ page: () => LoginView(),
+ binding: LoginBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.registerPage,
+ page: () => RegisterView(),
+ binding: RegisterBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.homePage,
+ page: () => HomeView(),
+ binding: HomeBinding(),
+ middlewares: [AuthMiddleware()],
+ ),
+ GetPage(
+ name: AppRoutes.mainPage,
+ page: () => NavbarView(),
+ bindings: [
+ NavbarBinding(),
+ HomeBinding(),
+ SearchBinding(),
+ LibraryBinding(),
+ HistoryBinding(),
+ ProfileBinding(),
+ ],
+ middlewares: [AuthMiddleware()],
+ ),
+ GetPage(
+ name: AppRoutes.quizCreatePage,
+ page: () => QuizCreationView(),
+ binding: QuizCreationBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.quizPreviewPage,
+ page: () => QuizPreviewPage(),
+ binding: QuizPreviewBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.detailQuizPage,
+ page: () => DetailQuizView(),
+ binding: DetailQuizBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.playQuizPage,
+ page: () => QuizPlayView(),
+ binding: QuizPlayBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.resultQuizPage,
+ page: () => QuizResultView(),
+ binding: QuizResultBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.listingQuizPage,
+ page: () => ListingsQuizView(),
+ binding: ListingQuizBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.detailHistoryPage,
+ page: () => DetailHistoryView(),
+ binding: DetailHistoryBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.roomPage,
+ page: () => RoomMakerView(),
+ binding: RoomMakerBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.waitRoomPage,
+ page: () => WaitingRoomView(),
+ binding: WaitingRoomBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.joinRoomPage,
+ page: () => JoinRoomView(),
+ binding: JoinRoomBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.monitorQuizMPLPage,
+ page: () => MonitorQuizView(),
+ binding: MonitorQuizBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.playQuizMPLPage,
+ page: () => PlayQuizMultiplayerView(),
+ binding: PlayQuizMultiplayerBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.updateProfilePage,
+ page: () => UpdateProfilePage(),
+ binding: UpdateProfileBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.monitorResultMPLPage,
+ page: () => AdminResultPage(),
+ binding: AdminResultBinding(),
+ ),
+ GetPage(
+ name: AppRoutes.quizMPLResultPage,
+ page: () => ParticipantDetailPage(),
+ binding: DetailParticipantResultBinding(),
+ )
+ ];
+}
diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart
new file mode 100644
index 0000000..c20d3af
--- /dev/null
+++ b/lib/app/routes/app_routes.dart
@@ -0,0 +1,32 @@
+part of 'app_pages.dart';
+
+abstract class AppRoutes {
+ static const splashScreen = "/splashscreen";
+ static const loginPage = "/login";
+ static const registerPage = "/register";
+ static const homePage = '/home';
+
+ static const mainPage = '/main';
+
+ static const quizCreatePage = "/quiz/creation";
+ static const quizPreviewPage = "/quiz/preview";
+ static const listingQuizPage = "/quiz/listing";
+ static const detailQuizPage = "/quiz/detail";
+
+ static const playQuizPage = "/quiz/play";
+ static const resultQuizPage = "/quiz/result";
+
+ static const detailHistoryPage = "/history/detail";
+
+ static const roomPage = "/room/quiz";
+ static const joinRoomPage = "/room/quiz/join";
+ static const waitRoomPage = "/room/quiz/waiting";
+
+ static const playQuizMPLPage = "/room/quiz/play";
+ static const monitorQuizMPLPage = "/room/quiz/monitor";
+ static const monitorResultMPLPage = "/room/quiz/monitor/result";
+
+ static const updateProfilePage = "/profile/update";
+
+ static const quizMPLResultPage = "/room/quiz/result";
+}
diff --git a/lib/component/app_name.dart b/lib/component/app_name.dart
new file mode 100644
index 0000000..0154c68
--- /dev/null
+++ b/lib/component/app_name.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+
+class AppName extends StatelessWidget {
+ final double fontSize;
+
+ const AppName({super.key, this.fontSize = 36});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ "GEN",
+ style: TextStyle(
+ fontSize: fontSize,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF172B4D),
+ letterSpacing: 1.2,
+ ),
+ ),
+ Text(
+ "SO",
+ style: TextStyle(
+ fontSize: fontSize,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF0052CC),
+ letterSpacing: 1.2,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/component/global_button.dart b/lib/component/global_button.dart
new file mode 100644
index 0000000..9effaae
--- /dev/null
+++ b/lib/component/global_button.dart
@@ -0,0 +1,66 @@
+import 'package:flutter/material.dart';
+
+enum ButtonType { primary, secondary, disabled }
+
+class GlobalButton extends StatelessWidget {
+ final VoidCallback? onPressed;
+ final String text;
+ final ButtonType type;
+ final Color baseColor;
+
+ const GlobalButton({
+ super.key,
+ required this.text,
+ required this.onPressed,
+ this.baseColor = const Color(0xFF0052CC),
+ this.type = ButtonType.primary,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final bool isDisabled = type == ButtonType.disabled || onPressed == null;
+
+ Color backgroundColor;
+ Color foregroundColor;
+ Color? borderColor;
+
+ switch (type) {
+ case ButtonType.primary:
+ backgroundColor = baseColor;
+ foregroundColor = Colors.white;
+ break;
+ case ButtonType.secondary:
+ backgroundColor = Colors.white;
+ foregroundColor = baseColor;
+ borderColor = const Color(0xFF0052CC);
+ break;
+ case ButtonType.disabled:
+ backgroundColor = const Color(0xFFE0E0E0);
+ foregroundColor = const Color(0xFF9E9E9E);
+ break;
+ }
+
+ return SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: backgroundColor,
+ foregroundColor: foregroundColor,
+ elevation: isDisabled ? 0 : 4,
+ shadowColor: !isDisabled ? backgroundColor.withValues(alpha: 0.3) : Colors.transparent,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ side: borderColor != null ? BorderSide(color: borderColor, width: 2) : BorderSide.none,
+ ),
+ textStyle: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ onPressed: isDisabled ? null : onPressed,
+ child: Text(text),
+ ),
+ );
+ }
+}
diff --git a/lib/component/global_dropdown_field.dart b/lib/component/global_dropdown_field.dart
new file mode 100644
index 0000000..9802887
--- /dev/null
+++ b/lib/component/global_dropdown_field.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+
+class GlobalDropdownField extends StatelessWidget {
+ final T value;
+ final List> items;
+ final ValueChanged onChanged;
+
+ const GlobalDropdownField({
+ super.key,
+ required this.value,
+ required this.items,
+ required this.onChanged,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
+ decoration: BoxDecoration(
+ color: const Color.fromARGB(255, 234, 234, 235),
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(
+ color: const Color(0xFF0052CC),
+ width: 1.5,
+ ),
+ ),
+ child: DropdownButtonHideUnderline(
+ child: DropdownButton(
+ value: value,
+ isExpanded: true,
+ onChanged: onChanged,
+ items: items,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/component/global_text_field.dart b/lib/component/global_text_field.dart
new file mode 100644
index 0000000..2e25c7f
--- /dev/null
+++ b/lib/component/global_text_field.dart
@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+
+class GlobalTextField extends StatelessWidget {
+ final TextEditingController controller;
+ final String? hintText;
+ final String? labelText;
+ final int limitTextLine;
+ final bool isPassword;
+ final bool obscureText;
+ final VoidCallback? onToggleVisibility;
+ final TextInputType textInputType;
+ final bool forceUpperCase;
+
+ const GlobalTextField(
+ {super.key,
+ required this.controller,
+ this.hintText,
+ this.labelText,
+ this.limitTextLine = 1,
+ this.isPassword = false,
+ this.obscureText = false,
+ this.onToggleVisibility,
+ this.forceUpperCase = false,
+ this.textInputType = TextInputType.text});
+
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ controller: controller,
+ keyboardType: textInputType,
+ obscureText: isPassword ? obscureText : false,
+ maxLines: limitTextLine,
+ textCapitalization: forceUpperCase ? TextCapitalization.characters : TextCapitalization.none,
+ decoration: InputDecoration(
+ labelText: labelText,
+ labelStyle: const TextStyle(
+ color: Color(0xFF6B778C),
+ fontSize: 14,
+ ),
+ hintText: hintText,
+ hintStyle: const TextStyle(
+ color: Color(0xFF6B778C),
+ fontSize: 14,
+ ),
+ filled: true,
+ fillColor: Color.fromARGB(255, 234, 234, 235),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(16),
+ borderSide: BorderSide.none,
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(16),
+ borderSide: BorderSide.none,
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(16),
+ borderSide: const BorderSide(
+ color: Color(0xFF0052CC),
+ width: 2,
+ ),
+ ),
+ suffixIcon: isPassword
+ ? IconButton(
+ icon: Icon(
+ obscureText ? Icons.visibility_off : Icons.visibility,
+ color: Color(0xFF6B778C),
+ ),
+ onPressed: onToggleVisibility,
+ )
+ : null,
+ ),
+ );
+ }
+}
diff --git a/lib/component/label_text_field.dart b/lib/component/label_text_field.dart
new file mode 100644
index 0000000..59f3008
--- /dev/null
+++ b/lib/component/label_text_field.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+
+class LabelTextField extends StatelessWidget {
+ final String label;
+ final double fontSize;
+ final FontWeight fontWeight;
+ final Alignment alignment;
+ final Color? color;
+
+ const LabelTextField({
+ super.key,
+ required this.label,
+ this.fontSize = 16,
+ this.fontWeight = FontWeight.bold,
+ this.alignment = Alignment.centerLeft,
+ this.color, // Tambahkan warna opsional
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: alignment,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), // padding lebih natural
+ child: Text(
+ label,
+ style: TextStyle(
+ fontSize: fontSize,
+ fontWeight: fontWeight,
+ color: color ?? const Color(0xFF172B4D), // default modern dark text
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/component/notification/delete_confirmation.dart b/lib/component/notification/delete_confirmation.dart
new file mode 100644
index 0000000..2cad6c2
--- /dev/null
+++ b/lib/component/notification/delete_confirmation.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:quiz_app/app/const/colors/app_colors.dart';
+
+class DeleteQuestionDialog {
+ static Future show({
+ required BuildContext context,
+ required VoidCallback onDelete,
+ }) async {
+ showDialog(
+ context: context,
+ barrierDismissible: true,
+ builder: (context) {
+ return Dialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Text(
+ "Hapus Soal?",
+ style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: AppColors.darkText),
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ "Soal ini akan dihapus dari daftar kuis. Yakin ingin menghapus?",
+ style: TextStyle(fontSize: 14, color: AppColors.softGrayText),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 24),
+ Row(
+ children: [
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () => Navigator.pop(context),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.white,
+ foregroundColor: AppColors.primaryBlue,
+ side: const BorderSide(color: AppColors.primaryBlue),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ ),
+ child: const Text("Batal"),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () {
+ Navigator.pop(context);
+ onDelete();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primaryBlue,
+ foregroundColor: Colors.white,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ ),
+ child: const Text("Hapus"),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/component/notification/pop_up_confirmation.dart b/lib/component/notification/pop_up_confirmation.dart
new file mode 100644
index 0000000..46cbe7e
--- /dev/null
+++ b/lib/component/notification/pop_up_confirmation.dart
@@ -0,0 +1,206 @@
+import 'package:flutter/material.dart';
+import 'package:quiz_app/app/const/colors/app_colors.dart';
+
+class AppDialog {
+ static Future showMessage(BuildContext context, String message) async {
+ await showDialog(
+ 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: [
+ const Icon(
+ Icons.info_outline,
+ size: 40,
+ color: AppColors.primaryBlue,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ message,
+ style: const TextStyle(
+ fontSize: 16,
+ color: AppColors.darkText,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ static Future showExitConfirmationDialog(BuildContext context) async {
+ await showDialog(
+ 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: [
+ const Text(
+ "Keluar tanpa menyimpan?",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: AppColors.darkText,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ "Perubahan yang belum disimpan akan hilang. Anda yakin ingin keluar?",
+ style: TextStyle(
+ fontSize: 14,
+ color: AppColors.softGrayText,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 24),
+ Row(
+ children: [
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () => Navigator.pop(context),
+ 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: const Text("Batal"),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () {
+ Navigator.pop(context); // Tutup dialog
+ Navigator.pop(context); // Kembali halaman
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primaryBlue,
+ foregroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ ),
+ child: const Text("Keluar"),
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ static Future showConfirmationDialog(
+ BuildContext context, {
+ required String title,
+ required String message,
+ String cancelText = "Batal",
+ String confirmText = "Yakin",
+ Color confirmColor = AppColors.primaryBlue,
+ }) async {
+ return showDialog(
+ 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),
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/component/quiz_container_component.dart b/lib/component/quiz_container_component.dart
new file mode 100644
index 0000000..d9fd6c6
--- /dev/null
+++ b/lib/component/quiz_container_component.dart
@@ -0,0 +1,100 @@
+import 'package:flutter/material.dart';
+import 'package:lucide_icons/lucide_icons.dart';
+import 'package:quiz_app/app/const/colors/app_colors.dart';
+import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
+
+class QuizContainerComponent extends StatelessWidget {
+ final QuizListingModel data;
+ final void Function(String quizId) onTap;
+
+ const QuizContainerComponent({
+ required this.data,
+ required this.onTap,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () => onTap(data.quizId),
+ child: Container(
+ padding: const EdgeInsets.all(14),
+ margin: EdgeInsets.symmetric(vertical: 5),
+ decoration: BoxDecoration(
+ color: AppColors.background,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: const Color(0xFFE1E4E8)),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.03),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildIconBox(),
+ const SizedBox(width: 12),
+ Expanded(child: _buildQuizInfo()),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildIconBox() {
+ return Container(
+ width: 50,
+ height: 50,
+ decoration: BoxDecoration(
+ color: const Color(0xFF0052CC),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Icon(LucideIcons.box, color: Colors.white, size: 28),
+ );
+ }
+
+ Widget _buildQuizInfo() {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ data.title,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF172B4D),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ 'Created by ${data.authorName}',
+ style: const TextStyle(
+ fontSize: 12,
+ color: Color(0xFF6B778C),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Icon(Icons.format_list_bulleted, size: 14, color: Color(0xFF6B778C)),
+ const SizedBox(width: 4),
+ Text(
+ '${data.totalQuiz} Quizzes',
+ style: const TextStyle(fontSize: 12, color: Color(0xFF6B778C)),
+ ),
+ const SizedBox(width: 12),
+ const Icon(Icons.access_time, size: 14, color: Color(0xFF6B778C)),
+ const SizedBox(width: 4),
+ Text(
+ '${data.duration} menit',
+ style: const TextStyle(fontSize: 12, color: Color(0xFF6B778C)),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/component/widget/container_skeleton_widget.dart b/lib/component/widget/container_skeleton_widget.dart
new file mode 100644
index 0000000..39f96aa
--- /dev/null
+++ b/lib/component/widget/container_skeleton_widget.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import 'package:shimmer/shimmer.dart';
+
+class ContainerSkeleton extends StatelessWidget {
+ const ContainerSkeleton({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.all(14),
+ margin: const EdgeInsets.symmetric(vertical: 5),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: const Color(0xFFE1E4E8)),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildBox(width: 50, height: 50, borderRadius: 8),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildBox(width: double.infinity, height: 16, borderRadius: 4),
+ const SizedBox(height: 6),
+ _buildBox(width: 100, height: 12, borderRadius: 4),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ _buildCircleBox(size: 14),
+ const SizedBox(width: 6),
+ _buildBox(width: 60, height: 10, borderRadius: 4),
+ const SizedBox(width: 12),
+ _buildCircleBox(size: 14),
+ const SizedBox(width: 6),
+ _buildBox(width: 60, height: 10, borderRadius: 4),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildBox({
+ required double width,
+ required double height,
+ double borderRadius = 6,
+ }) {
+ return Shimmer.fromColors(
+ baseColor: Colors.grey.shade300,
+ highlightColor: Colors.grey.shade100,
+ child: Container(
+ width: width,
+ height: height,
+ decoration: BoxDecoration(
+ color: Colors.grey,
+ borderRadius: BorderRadius.circular(borderRadius),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCircleBox({required double size}) {
+ return Shimmer.fromColors(
+ baseColor: Colors.grey.shade300,
+ highlightColor: Colors.grey.shade100,
+ child: Container(
+ width: size,
+ height: size,
+ decoration: const BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.grey,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/component/widget/loading_widget.dart b/lib/component/widget/loading_widget.dart
new file mode 100644
index 0000000..35b24c8
--- /dev/null
+++ b/lib/component/widget/loading_widget.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+
+class LoadingWidget extends StatelessWidget {
+ const LoadingWidget({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 12),
+ Text(
+ "Memuat data...",
+ style: TextStyle(color: Colors.grey, fontSize: 14),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/component/widget/question_container_widget.dart b/lib/component/widget/question_container_widget.dart
new file mode 100644
index 0000000..448652b
--- /dev/null
+++ b/lib/component/widget/question_container_widget.dart
@@ -0,0 +1,245 @@
+import 'package:flutter/material.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/data/models/quiz/quiestion_data_model.dart';
+import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
+
+class QuestionContainerWidget extends StatelessWidget {
+ final QuestionData question;
+ final AnsweredQuestion? answeredQuestion;
+
+ const QuestionContainerWidget({
+ super.key,
+ required this.question,
+ this.answeredQuestion,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: double.infinity,
+ margin: const EdgeInsets.only(bottom: 20),
+ padding: const EdgeInsets.all(16),
+ decoration: _containerDecoration,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildTitle(),
+ const SizedBox(height: 6),
+ _buildTypeLabel(),
+ const SizedBox(height: 12),
+ _buildQuestionText(),
+ const SizedBox(height: 16),
+ _buildAnswerSection(),
+ if (answeredQuestion != null) ...[
+ const SizedBox(height: 16),
+ _buildAnsweredSection(question, answeredQuestion!),
+ ],
+ const SizedBox(height: 10),
+ _buildDurationInfo(),
+ ],
+ ),
+ );
+ }
+
+ // --- UI Builders ---
+
+ Widget _buildTitle() => Text(
+ 'Soal ${question.index}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: AppColors.darkText,
+ ),
+ );
+
+ Widget _buildTypeLabel() => Text(
+ _mapQuestionTypeToText(question.type),
+ style: const TextStyle(
+ fontSize: 12,
+ color: AppColors.softGrayText,
+ fontStyle: FontStyle.italic,
+ ),
+ );
+
+ Widget _buildQuestionText() => Text(
+ question.question ?? '-',
+ style: const TextStyle(fontSize: 16, color: AppColors.darkText),
+ );
+
+ Widget _buildAnswerSection() {
+ switch (question.type) {
+ case QuestionType.option:
+ return _buildOptionAnswers();
+ case QuestionType.fillTheBlank:
+ return _buildFillInBlankAnswers();
+ case QuestionType.trueOrFalse:
+ return _buildTrueFalseAnswer();
+ default:
+ return const SizedBox();
+ }
+ }
+
+ Widget _buildOptionAnswers() {
+ final options = question.options ?? [];
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: options.map((option) {
+ final isCorrect = option.index == question.correctAnswerIndex;
+ return _buildOptionItem(option.text, isCorrect);
+ }).toList(),
+ );
+ }
+
+ Widget _buildOptionItem(String text, bool isCorrect) {
+ return Container(
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
+ decoration: BoxDecoration(
+ color: isCorrect ? AppColors.primaryBlue.withValues(alpha: 0.1) : Colors.transparent,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ isCorrect ? Icons.check_circle_rounded : Icons.circle_outlined,
+ size: 18,
+ color: isCorrect ? AppColors.primaryBlue : AppColors.softGrayText,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ text,
+ style: TextStyle(
+ fontWeight: isCorrect ? FontWeight.bold : FontWeight.normal,
+ color: isCorrect ? AppColors.primaryBlue : AppColors.darkText,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildFillInBlankAnswers() {
+ final variations = _generateFillBlankVariations(question.answer ?? '-');
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: variations.map((text) => _buildBulletText(text)).toList(),
+ );
+ }
+
+ Widget _buildTrueFalseAnswer() {
+ return Text(
+ 'Jawaban: ${question.answer ?? '-'}',
+ style: const TextStyle(color: AppColors.softGrayText),
+ );
+ }
+
+ Widget _buildAnsweredSection(QuestionData question, AnsweredQuestion answered) {
+ String answer = question.type == QuestionType.option ? question.options![int.parse(answered.selectedAnswer)].text : answered.selectedAnswer;
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Jawaban Anda:',
+ style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.darkText),
+ ),
+ const SizedBox(height: 6),
+ Row(
+ children: [
+ Icon(
+ answered.isCorrect ? Icons.check_circle : Icons.cancel,
+ color: answered.isCorrect ? Colors.green : Colors.red,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ answer,
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ color: answered.isCorrect ? Colors.green : Colors.red,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Widget _buildBulletText(String text) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2),
+ child: Row(
+ children: [
+ const Icon(Icons.arrow_right, size: 18, color: AppColors.softGrayText),
+ const SizedBox(width: 6),
+ Text(
+ text,
+ style: const TextStyle(color: AppColors.darkText),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildDurationInfo() {
+ String duration = question.duration.toString();
+ if (answeredQuestion != null) duration = answeredQuestion!.duration.toString();
+
+ return Text(
+ 'Durasi: $duration detik',
+ style: const TextStyle(fontSize: 14, color: AppColors.softGrayText),
+ );
+ }
+
+ // --- Utils ---
+
+ List _generateFillBlankVariations(String answer) {
+ return [
+ _capitalizeEachWord(answer),
+ answer.toLowerCase(),
+ _capitalizeFirstWordOnly(answer),
+ ];
+ }
+
+ String _capitalizeEachWord(String text) {
+ return text.split(' ').map((w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}' : '').join(' ');
+ }
+
+ String _capitalizeFirstWordOnly(String text) {
+ final parts = text.split(' ');
+ if (parts.isEmpty) return text;
+ parts[0] = _capitalizeEachWord(parts[0]);
+ for (int i = 1; i < parts.length; i++) {
+ parts[i] = parts[i].toLowerCase();
+ }
+ return parts.join(' ');
+ }
+
+ String _mapQuestionTypeToText(QuestionType? type) {
+ return switch (type) {
+ QuestionType.option => 'Tipe: Pilihan Ganda',
+ QuestionType.fillTheBlank => 'Tipe: Isian Kosong',
+ QuestionType.trueOrFalse => 'Tipe: Benar / Salah',
+ _ => 'Tipe: Tidak diketahui',
+ };
+ }
+
+ BoxDecoration get _containerDecoration => BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: AppColors.borderLight),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.05),
+ blurRadius: 6,
+ offset: const Offset(2, 2),
+ ),
+ ],
+ );
+}
diff --git a/lib/component/widget/quiz_item_wa_component.dart b/lib/component/widget/quiz_item_wa_component.dart
new file mode 100644
index 0000000..04caebd
--- /dev/null
+++ b/lib/component/widget/quiz_item_wa_component.dart
@@ -0,0 +1,288 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:lucide_icons/lucide_icons.dart';
+import 'package:quiz_app/app/const/colors/app_colors.dart';
+import 'package:quiz_app/app/const/text/text_style.dart';
+
+/// Single quiz result tile.
+/// Shows the question, the user's answer, correctness, time spent, and per‑option feedback.
+///
+/// * Text strings are fully localised via `easy_localization`.
+/// * Long answers now wrap to the next line rather than overflowing.
+/// * Option chips highlight both the correct answer and the user's incorrect choice (if any).
+class QuizItemWAComponent extends StatelessWidget {
+ const QuizItemWAComponent({
+ super.key,
+ required this.index,
+ required this.question,
+ required this.type,
+ required this.userAnswer,
+ required this.targetAnswer,
+ required this.isCorrect,
+ required this.timeSpent,
+ this.options,
+ });
+
+ /// One‑based question index.
+ final int index;
+
+ /// The question text.
+ final String question;
+
+ /// Question type: `option`, `true_false`, or `fill_the_blank`.
+ final String type;
+
+ /// Raw user answer (index, bool or string). `-1`/`null` means no answer.
+ final dynamic userAnswer;
+
+ /// Raw correct answer (index, bool or string).
+ final dynamic targetAnswer;
+
+ /// Whether the user answered correctly.
+ final bool isCorrect;
+
+ /// Time spent answering (seconds).
+ final double timeSpent;
+
+ /// Option texts for option‑type questions.
+ final List? options;
+
+ bool get _isOptionType => type == 'option';
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: double.infinity,
+ margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(.04),
+ blurRadius: 6,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ————————————————— Question text
+ Text(
+ '$index. $question',
+ softWrap: true,
+ style: AppTextStyles.title.copyWith(fontSize: 16, fontWeight: FontWeight.w600),
+ ),
+
+ if (_isOptionType && options != null) ...[
+ const SizedBox(height: 16),
+ _OptionsList(
+ options: options!,
+ userAnswer: userAnswer,
+ targetAnswer: targetAnswer,
+ ),
+ ],
+
+ const SizedBox(height: 12),
+ _AnswerIndicator(
+ isCorrect: isCorrect,
+ isAnswered: userAnswer != null && userAnswer != -1,
+ userAnswerText: _buildUserAnswerText(),
+ correctAnswerText: _buildCorrectAnswerText(),
+ ),
+ const SizedBox(height: 16),
+ const Divider(height: 24, color: AppColors.shadowPrimary),
+ _MetaBar(type: type, timeSpent: timeSpent),
+ ],
+ ),
+ );
+ }
+
+ // ——————————————————————————————————————————————————————— Helpers
+ String _buildUserAnswerText() {
+ if (userAnswer == null || userAnswer == -1) {
+ return tr('not_answered');
+ }
+ if (_isOptionType) {
+ final idx = int.tryParse(userAnswer.toString()) ?? -1;
+ if (idx >= 0 && idx < (options?.length ?? 0)) return options![idx];
+ }
+ return userAnswer.toString();
+ }
+
+ String _buildCorrectAnswerText() {
+ if (_isOptionType && options != null) {
+ final idx = int.tryParse(targetAnswer.toString()) ?? -1;
+ if (idx >= 0 && idx < options!.length) return options![idx];
+ }
+ return targetAnswer.toString();
+ }
+}
+
+// ————————————————————————————————————————————————————————— Sub‑widgets
+
+class _OptionsList extends StatelessWidget {
+ const _OptionsList({
+ required this.options,
+ required this.userAnswer,
+ required this.targetAnswer,
+ });
+
+ final List options;
+ final dynamic userAnswer;
+ final dynamic targetAnswer;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: List.generate(options.length, (i) {
+ final text = options[i];
+ final bool isCorrectAnswer = i == targetAnswer;
+ final bool isUserWrongAnswer = i == userAnswer && !isCorrectAnswer;
+
+ Color? bg;
+ IconData icon = LucideIcons.circle;
+ Color iconColor = AppColors.shadowPrimary;
+
+ if (isCorrectAnswer) {
+ bg = AppColors.primaryBlue.withOpacity(.15);
+ icon = LucideIcons.checkCircle2;
+ iconColor = AppColors.primaryBlue;
+ } else if (isUserWrongAnswer) {
+ bg = Colors.red.withOpacity(.15);
+ icon = LucideIcons.xCircle;
+ iconColor = Colors.red;
+ }
+
+ return Container(
+ width: double.infinity,
+ margin: const EdgeInsets.symmetric(vertical: 6),
+ padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
+ decoration: BoxDecoration(
+ color: bg,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: AppColors.shadowPrimary),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, size: 16, color: iconColor),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ text,
+ style: AppTextStyles.optionText,
+ softWrap: true,
+ ),
+ ),
+ ],
+ ),
+ );
+ }),
+ );
+ }
+}
+
+class _AnswerIndicator extends StatelessWidget {
+ const _AnswerIndicator({
+ required this.isCorrect,
+ required this.isAnswered,
+ required this.userAnswerText,
+ required this.correctAnswerText,
+ });
+
+ final bool isCorrect;
+ final bool isAnswered;
+ final String userAnswerText;
+ final String correctAnswerText;
+
+ @override
+ Widget build(BuildContext context) {
+ final icon = isCorrect ? LucideIcons.checkCircle2 : LucideIcons.xCircle;
+ final color = isCorrect ? AppColors.primaryBlue : Colors.red;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: color, size: 18),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ // "Jawabanmu: "
+ tr('your_answer', namedArgs: {'answer': userAnswerText}),
+ style: AppTextStyles.statValue,
+ softWrap: true,
+ ),
+ ),
+ ],
+ ),
+ if (!isCorrect && isAnswered) ...[
+ const SizedBox(height: 6),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(width: 26), // align with text above
+ Expanded(
+ child: Text(
+ // "Jawaban benar: "
+ tr('correct_answer', namedArgs: {'answer': correctAnswerText}),
+
+ style: AppTextStyles.caption,
+ softWrap: true,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ],
+ );
+ }
+}
+
+class _MetaBar extends StatelessWidget {
+ const _MetaBar({required this.type, required this.timeSpent});
+ final String type;
+ final double timeSpent;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _MetaItem(
+ icon: LucideIcons.helpCircle,
+ label: tr(
+ 'quiz_type_$type',
+ ),
+ ),
+ _MetaItem(
+ icon: LucideIcons.clock3,
+ label: '${timeSpent.toStringAsFixed(1)}${tr('seconds_suffix')}',
+ ),
+ ],
+ );
+ }
+}
+
+class _MetaItem extends StatelessWidget {
+ const _MetaItem({required this.icon, required this.label});
+
+ final IconData icon;
+ final String label;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Icon(icon, size: 16, color: AppColors.primaryBlue),
+ const SizedBox(width: 6),
+ Text(label, style: AppTextStyles.caption),
+ ],
+ );
+ }
+}
diff --git a/lib/component/widget/recomendation_component.dart b/lib/component/widget/recomendation_component.dart
new file mode 100644
index 0000000..203e461
--- /dev/null
+++ b/lib/component/widget/recomendation_component.dart
@@ -0,0 +1,83 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:quiz_app/component/quiz_container_component.dart';
+import 'package:quiz_app/component/widget/container_skeleton_widget.dart';
+import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
+
+class RecomendationComponent extends StatelessWidget {
+ final String title;
+ final List datas;
+ final Function(String) itemOnTap;
+ final Function() allOnTap;
+
+ const RecomendationComponent({
+ required this.title,
+ required this.datas,
+ required this.itemOnTap,
+ required this.allOnTap,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildSectionTitle(context, title),
+ const SizedBox(height: 10),
+ datas.isNotEmpty
+ ? ListView.builder(
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ itemCount: datas.length,
+ itemBuilder: (context, index) => QuizContainerComponent(
+ data: datas[index],
+ onTap: itemOnTap,
+ ),
+ )
+ : ListView.builder(
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ itemCount: 3,
+ itemBuilder: (context, index) => ContainerSkeleton(),
+ )
+ ],
+ );
+ }
+
+ // Widget _label() {
+ // return const Padding(
+ // padding: EdgeInsets.symmetric(horizontal: 16),
+ // child: Text(
+ // "Quiz Recommendation",
+ // style: TextStyle(
+ // fontSize: 18,
+ // fontWeight: FontWeight.bold,
+ // color: Color(0xFF172B4D), // dark text
+ // ),
+ // ),
+ // );
+ // }
+
+ Widget _buildSectionTitle(BuildContext context, String title) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ title,
+ style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ GestureDetector(
+ onTap: allOnTap,
+ child: Text(
+ context.tr('see_all'),
+ style: TextStyle(fontSize: 14, color: Colors.blue.shade700),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart
new file mode 100644
index 0000000..bd4e5ee
--- /dev/null
+++ b/lib/core/endpoint/api_endpoint.dart
@@ -0,0 +1,33 @@
+class APIEndpoint {
+ // static const String baseUrl = "http://192.168.1.13:5000";
+ static const String baseUrl = "http://103.193.178.121:5000";
+ static const String api = "$baseUrl/api";
+
+ static const String login = "/login";
+ static const String loginGoogle = "/login/google";
+
+ static const String register = "/register";
+
+ static const String quiz = "/quiz";
+ static const String quizGenerate = "/quiz/ai";
+ static const String quizAnswer = "/quiz/answer";
+ static const String quizAnswerSession = "/quiz/answer/session";
+
+ static const String userQuiz = "/quiz/user";
+ static const String quizPopuler = "/quiz/populer";
+ static const String quizRecommendation = "/quiz/recommendation";
+ static const String quizSearch = "/quiz/search";
+
+ static const String historyQuiz = "/history";
+ static const String detailHistoryQuiz = "/history/detail";
+
+ static const String subject = "/subject";
+
+ static const String session = "/session";
+
+ static const String sessionHistory = "/history/session";
+
+ static const String userData = "/user";
+ static const String userUpdate = "/user/update";
+ static const String userStat = "/user/status";
+}
diff --git a/lib/core/helper/connection_check.dart b/lib/core/helper/connection_check.dart
new file mode 100644
index 0000000..a35d433
--- /dev/null
+++ b/lib/core/helper/connection_check.dart
@@ -0,0 +1,17 @@
+import 'package:quiz_app/core/utils/custom_notification.dart';
+
+class ConnectionNotification {
+ static void internetConnected() {
+ CustomNotification.success(
+ title: "Terkoneksi kembali",
+ message: "Terhubugn dengan koneksi",
+ );
+ }
+
+ static void noInternedConnection() {
+ CustomNotification.error(
+ title: "Tidak ada internet",
+ message: "cek kembali koneksi internet kamu",
+ );
+ }
+}
diff --git a/lib/core/helper/responsive.dart b/lib/core/helper/responsive.dart
new file mode 100644
index 0000000..007718c
--- /dev/null
+++ b/lib/core/helper/responsive.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+
+class SizeConfig {
+ late double screenWidth;
+ late double screenHeight;
+ double baseSize = 8.0;
+
+ SizeConfig(BuildContext context) {
+ final mediaQueryData = MediaQuery.of(context);
+ screenWidth = mediaQueryData.size.width;
+ screenHeight = mediaQueryData.size.height;
+ }
+
+ double size(double multiplier) {
+ return baseSize * multiplier;
+ }
+
+ double height(double multiplier) {
+ return screenHeight * (multiplier / 100);
+ }
+
+ double width(double multiplier) {
+ return screenWidth * (multiplier / 100);
+ }
+}
diff --git a/lib/core/utils/custom_floating_loading.dart b/lib/core/utils/custom_floating_loading.dart
new file mode 100644
index 0000000..0bf1aa4
--- /dev/null
+++ b/lib/core/utils/custom_floating_loading.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+
+class CustomFloatingLoading {
+ static OverlayEntry? _overlayEntry;
+
+ static void showLoading(BuildContext context) {
+ if (_overlayEntry != null) return;
+
+ _overlayEntry = OverlayEntry(
+ builder: (_) => Stack(
+ children: [
+ ModalBarrier(
+ dismissible: false,
+ color: Colors.black.withValues(alpha: 0.5),
+ ),
+ const Center(
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ );
+
+ Overlay.of(context).insert(_overlayEntry!);
+ }
+
+ static void hideLoading() {
+ if (_overlayEntry?.mounted == true) {
+ _overlayEntry?.remove();
+ }
+ _overlayEntry = null;
+ }
+}
diff --git a/lib/core/utils/custom_notification.dart b/lib/core/utils/custom_notification.dart
new file mode 100644
index 0000000..f69c5ca
--- /dev/null
+++ b/lib/core/utils/custom_notification.dart
@@ -0,0 +1,63 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+class CustomNotification {
+ static void _showSnackbar({
+ required String title,
+ required String message,
+ required IconData icon,
+ required Color backgroundColor,
+ Color textColor = Colors.white,
+ Color iconColor = Colors.white,
+ }) {
+ Get.snackbar(
+ title,
+ message,
+ icon: Icon(icon, color: iconColor),
+ backgroundColor: backgroundColor,
+ colorText: textColor,
+ borderRadius: 12,
+ margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ snackPosition: SnackPosition.TOP,
+ duration: const Duration(seconds: 3),
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
+ isDismissible: true,
+ forwardAnimationCurve: Curves.easeOutBack,
+ reverseAnimationCurve: Curves.easeInBack,
+ boxShadows: [
+ BoxShadow(
+ color: Colors.black26,
+ blurRadius: 10,
+ offset: Offset(0, 4),
+ ),
+ ],
+ );
+ }
+
+ static void success({required String title, required String message}) {
+ _showSnackbar(
+ title: title,
+ message: message,
+ icon: Icons.check_circle_outline,
+ backgroundColor: Colors.green.shade600,
+ );
+ }
+
+ static void error({required String title, required String message}) {
+ _showSnackbar(
+ title: title,
+ message: message,
+ icon: Icons.error_outline,
+ backgroundColor: Colors.red.shade600,
+ );
+ }
+
+ static void warning({required String title, required String message}) {
+ _showSnackbar(
+ title: title,
+ message: message,
+ icon: Icons.warning_amber_rounded,
+ backgroundColor: Colors.orange.shade700,
+ );
+ }
+}
diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart
new file mode 100644
index 0000000..a5b7695
--- /dev/null
+++ b/lib/core/utils/logger.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/foundation.dart'; // For kDebugMode
+import 'package:logger/logger.dart';
+
+class AppLogger {
+ static final Logger _debugLogger = Logger(
+ printer: PrettyPrinter(
+ errorMethodCount: 5,
+ colors: true,
+ printEmojis: false,
+ dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
+ ),
+ );
+
+ static final Logger _releaseLogger = Logger(
+ printer: SimplePrinter(),
+ );
+
+ static Logger get instance => kDebugMode ? _debugLogger : _releaseLogger;
+}
+
+/// debug print custom
+Logger get logC => AppLogger.instance;
diff --git a/lib/data/controllers/user_controller.dart b/lib/data/controllers/user_controller.dart
new file mode 100644
index 0000000..0ee2a54
--- /dev/null
+++ b/lib/data/controllers/user_controller.dart
@@ -0,0 +1,49 @@
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'package:get/get_state_manager/src/simple/get_controllers.dart';
+import 'package:quiz_app/core/utils/logger.dart';
+import 'package:quiz_app/data/entity/user/user_entity.dart';
+import 'package:quiz_app/data/services/user_storage_service.dart';
+
+class UserController extends GetxController {
+ final UserStorageService _userStorageService;
+
+ UserController(this._userStorageService);
+
+ Rx userName = "".obs;
+ Rx userImage = Rx(null);
+ Rx email = "".obs;
+
+ UserEntity? userData;
+
+ @override
+ void onInit() {
+ loadUser();
+ super.onInit();
+ }
+
+ Future loadUser() async {
+ final data = await _userStorageService.loadUser();
+ if (data != null) {
+ userData = data;
+ userName.value = data.name;
+ userImage.value = data.picUrl;
+ email.value = data.email;
+ logC.i("Loaded user: ${data.toJson()}");
+ }
+ }
+
+ void setUserFromEntity(UserEntity data) {
+ final userEntity = data;
+ userData = userEntity;
+ userName.value = userEntity.name;
+ userImage.value = userEntity.picUrl;
+ email.value = userEntity.email;
+ }
+
+ void clearUser() {
+ userData = null;
+ userName.value = "";
+ userImage.value = "";
+ email.value = '';
+ }
+}
diff --git a/lib/data/dto/waiting_room_dto.dart b/lib/data/dto/waiting_room_dto.dart
new file mode 100644
index 0000000..bad643e
--- /dev/null
+++ b/lib/data/dto/waiting_room_dto.dart
@@ -0,0 +1,17 @@
+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_response_model.dart';
+
+class WaitingRoomDTO {
+ final bool isAdmin;
+ final SessionResponseModel data;
+ final SessionInfo sessionInfo;
+ final QuizInfo quizInfo;
+
+ WaitingRoomDTO({
+ required this.isAdmin,
+ required this.data,
+ required this.sessionInfo,
+ required this.quizInfo,
+ });
+}
diff --git a/lib/data/entity/user/user_entity.dart b/lib/data/entity/user/user_entity.dart
new file mode 100644
index 0000000..fd3b375
--- /dev/null
+++ b/lib/data/entity/user/user_entity.dart
@@ -0,0 +1,47 @@
+class UserEntity {
+ final String id;
+ final String name;
+ final String email;
+ final String? picUrl;
+ final String? birthDate;
+ final String? locale;
+ final String? phone;
+ final String? createdAt;
+
+ UserEntity({
+ required this.id,
+ required this.name,
+ required this.email,
+ this.picUrl,
+ this.birthDate,
+ this.locale,
+ this.phone,
+ this.createdAt,
+ });
+
+ factory UserEntity.fromJson(Map json) {
+ return UserEntity(
+ id: json['id'] ?? '',
+ name: json['name'] ?? '',
+ email: json['email'] ?? '',
+ picUrl: json['pic_url'],
+ birthDate: json['birth_date'],
+ locale: json['locale'],
+ phone: json['phone'],
+ createdAt: json['created_at'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'email': email,
+ 'pic_url': picUrl,
+ 'birth_date': birthDate,
+ 'locale': locale,
+ 'phone': phone,
+ "created_at": createdAt,
+ };
+ }
+}
diff --git a/lib/data/models/answer/answer_model.dart b/lib/data/models/answer/answer_model.dart
new file mode 100644
index 0000000..10c2bd6
--- /dev/null
+++ b/lib/data/models/answer/answer_model.dart
@@ -0,0 +1,31 @@
+class AnswerModel {
+ final int questionIndex;
+ final dynamic answer; // String, bool, atau int
+ final bool isCorrect;
+ final double timeSpent;
+
+ AnswerModel({
+ required this.questionIndex,
+ required this.answer,
+ required this.isCorrect,
+ required this.timeSpent,
+ });
+
+ factory AnswerModel.fromJson(Map json) {
+ return AnswerModel(
+ questionIndex: json['question_index'],
+ answer: json['answer'],
+ isCorrect: json['is_correct'],
+ timeSpent: (json['time_spent'] as num).toDouble(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'question_index': questionIndex,
+ 'answer': answer,
+ 'is_correct': isCorrect,
+ 'time_spent': timeSpent,
+ };
+ }
+}
diff --git a/lib/data/models/answer/quiz_answer_submition_model.dart b/lib/data/models/answer/quiz_answer_submition_model.dart
new file mode 100644
index 0000000..980ed7d
--- /dev/null
+++ b/lib/data/models/answer/quiz_answer_submition_model.dart
@@ -0,0 +1,37 @@
+import 'package:quiz_app/data/models/answer/answer_model.dart';
+
+class QuizAnswerSubmissionModel {
+ final String sessionId;
+ final String quizId;
+ final String userId;
+ final DateTime answeredAt;
+ final List answers;
+
+ QuizAnswerSubmissionModel({
+ required this.sessionId,
+ required this.quizId,
+ required this.userId,
+ required this.answeredAt,
+ required this.answers,
+ });
+
+ factory QuizAnswerSubmissionModel.fromJson(Map json) {
+ return QuizAnswerSubmissionModel(
+ sessionId: json['session_id'],
+ quizId: json['quiz_id'],
+ userId: json['user_id'],
+ answeredAt: DateTime.parse(json['answered_at']),
+ answers: (json['answers'] as List).map((e) => AnswerModel.fromJson(e)).toList(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'session_id': sessionId,
+ 'quiz_id': quizId,
+ 'user_id': userId,
+ 'answered_at': answeredAt.toIso8601String(),
+ 'answers': answers.map((e) => e.toJson()).toList(),
+ };
+ }
+}
diff --git a/lib/data/models/base/base_model.dart b/lib/data/models/base/base_model.dart
new file mode 100644
index 0000000..d5aac4e
--- /dev/null
+++ b/lib/data/models/base/base_model.dart
@@ -0,0 +1,54 @@
+class BaseResponseModel {
+ final String message;
+ final T? data;
+ final MetaModel? meta;
+
+ BaseResponseModel({
+ required this.message,
+ this.data,
+ this.meta,
+ });
+
+ factory BaseResponseModel.fromJson(
+ Map json,
+ T Function(dynamic) fromJsonT,
+ ) {
+ return BaseResponseModel(
+ message: json['message'],
+ data: json['data'] != null ? fromJsonT(json['data']) : null,
+ meta: json['meta'] != null ? MetaModel.fromJson(json['meta']) : null,
+ );
+ }
+}
+
+class MetaModel {
+ final int totalPage;
+ final int currentPage;
+ final int totalData;
+ final int totalAllData;
+
+ MetaModel({
+ required this.totalPage,
+ required this.currentPage,
+ required this.totalData,
+ required this.totalAllData,
+ });
+
+ factory MetaModel.fromJson(Map json) {
+ return MetaModel(
+ totalPage: json['total_page'],
+ currentPage: json['current_page'],
+ totalData: json['total_data'],
+ totalAllData: json['total_all_data'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'total_page': totalPage,
+ 'current_page': currentPage,
+ 'total_data': totalData,
+ 'total_all_data': totalAllData,
+ };
+ }
+}
diff --git a/lib/data/models/history/detail_quiz_history.dart b/lib/data/models/history/detail_quiz_history.dart
new file mode 100644
index 0000000..226fb46
--- /dev/null
+++ b/lib/data/models/history/detail_quiz_history.dart
@@ -0,0 +1,75 @@
+class QuizAnswerResult {
+ final String answerId;
+ final String quizId;
+ final String title;
+ final String description;
+ final String authorId;
+ final String answeredAt;
+ final int totalCorrect;
+ final int totalScore;
+ final double totalSolveTime;
+ final List questionListings;
+
+ QuizAnswerResult({
+ required this.answerId,
+ required this.quizId,
+ required this.title,
+ required this.description,
+ required this.authorId,
+ required this.answeredAt,
+ required this.totalCorrect,
+ required this.totalScore,
+ required this.totalSolveTime,
+ required this.questionListings,
+ });
+
+ factory QuizAnswerResult.fromJson(Map json) {
+ return QuizAnswerResult(
+ answerId: json['answer_id'],
+ quizId: json['quiz_id'],
+ title: json['title'],
+ description: json['description'],
+ authorId: json['author_id'],
+ answeredAt: json['answered_at'],
+ totalCorrect: json['total_correct'],
+ totalScore: json['total_score'],
+ totalSolveTime: (json['total_solve_time'] as num).toDouble(),
+ questionListings: (json['question_listings'] as List).map((e) => QuestionAnswerItem.fromJson(e)).toList(),
+ );
+ }
+}
+
+class QuestionAnswerItem {
+ final int index;
+ final String question;
+ final String type;
+ final dynamic targetAnswer;
+ final dynamic userAnswer;
+ final bool isCorrect;
+ final double timeSpent;
+ final List? options;
+
+ QuestionAnswerItem({
+ required this.index,
+ required this.question,
+ required this.type,
+ required this.targetAnswer,
+ required this.userAnswer,
+ required this.isCorrect,
+ required this.timeSpent,
+ this.options,
+ });
+
+ factory QuestionAnswerItem.fromJson(Map json) {
+ return QuestionAnswerItem(
+ index: json['index'],
+ question: json['question'],
+ type: json['type'],
+ targetAnswer: json['target_answer'],
+ userAnswer: json['user_answer'],
+ isCorrect: json['is_correct'],
+ timeSpent: (json['time_spent'] as num).toDouble(),
+ options: json['options'] != null ? List.from(json['options']) : null,
+ );
+ }
+}
diff --git a/lib/data/models/history/participant_history_result.dart b/lib/data/models/history/participant_history_result.dart
new file mode 100644
index 0000000..dad6a43
--- /dev/null
+++ b/lib/data/models/history/participant_history_result.dart
@@ -0,0 +1,78 @@
+class QuestionAnswer {
+ final int index;
+ final String question;
+ final dynamic targetAnswer;
+ final int duration;
+ final String type;
+ final List? options;
+ final String answer;
+ final bool isCorrect;
+ final double timeSpent;
+
+ QuestionAnswer({
+ required this.index,
+ required this.question,
+ required this.targetAnswer,
+ required this.duration,
+ required this.type,
+ required this.options,
+ required this.answer,
+ required this.isCorrect,
+ required this.timeSpent,
+ });
+
+ factory QuestionAnswer.fromJson(Map json) {
+ return QuestionAnswer(
+ index: json['index'],
+ question: json['question'],
+ targetAnswer: json['target_answer'],
+ duration: json['duration'],
+ type: json['type'],
+ options: json['options'] != null ? List.from(json['options']) : null,
+ answer: json['answer'],
+ isCorrect: json['is_correct'],
+ timeSpent: (json['time_spent'] as num).toDouble(),
+ );
+ }
+}
+
+class ParticipantResult {
+ final String id;
+ final String sessionId;
+ final String quizId;
+ final String userId;
+ final String answeredAt;
+ final List answers;
+ final int totalScore;
+ final int totalCorrect;
+
+ ParticipantResult({
+ required this.id,
+ required this.sessionId,
+ required this.quizId,
+ required this.userId,
+ required this.answeredAt,
+ required this.answers,
+ required this.totalScore,
+ required this.totalCorrect,
+ });
+
+ factory ParticipantResult.fromJson(Map json) {
+ return ParticipantResult(
+ id: json['id'],
+ sessionId: json['session_id'],
+ quizId: json['quiz_id'],
+ userId: json['user_id'],
+ answeredAt: json['answered_at'],
+ answers: (json['answers'] as List).map((e) => QuestionAnswer.fromJson(e)).toList(),
+ totalScore: json['total_score'],
+ totalCorrect: json['total_correct'],
+ );
+ }
+
+ double get scorePercent => (totalCorrect / answers.length) * 100;
+
+ int get totalQuestions => answers.length;
+
+ String get name => "User $userId";
+}
diff --git a/lib/data/models/history/quiz_history.dart b/lib/data/models/history/quiz_history.dart
new file mode 100644
index 0000000..5a73af1
--- /dev/null
+++ b/lib/data/models/history/quiz_history.dart
@@ -0,0 +1,42 @@
+class QuizHistory {
+ final String quizId;
+ final String answerId;
+ final String title;
+ final String description;
+ final int totalCorrect;
+ final int totalQuestion;
+ final String date;
+
+ QuizHistory({
+ required this.quizId,
+ required this.answerId,
+ required this.title,
+ required this.description,
+ required this.totalCorrect,
+ required this.totalQuestion,
+ required this.date,
+ });
+
+ factory QuizHistory.fromJson(Map json) {
+ return QuizHistory(
+ quizId: json['quiz_id'],
+ answerId: json['answer_id'],
+ title: json['title'],
+ description: json['description'],
+ totalCorrect: json['total_correct'],
+ totalQuestion: json['total_question'],
+ date: json["date"]);
+ }
+
+ Map toJson() {
+ return {
+ 'quiz_id': quizId,
+ 'answer_id': answerId,
+ 'title': title,
+ 'description': description,
+ 'total_correct': totalCorrect,
+ 'total_question': totalQuestion,
+ 'date': date
+ };
+ }
+}
diff --git a/lib/data/models/history/session_history.dart b/lib/data/models/history/session_history.dart
new file mode 100644
index 0000000..4391410
--- /dev/null
+++ b/lib/data/models/history/session_history.dart
@@ -0,0 +1,87 @@
+class Participant {
+ final String id;
+ final String name;
+ final int score;
+
+ Participant({
+ required this.id,
+ required this.name,
+ required this.score,
+ });
+
+ factory Participant.fromJson(Map json) {
+ return Participant(
+ id: json['id'],
+ name: json['name'],
+ score: json['score'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'score': score,
+ };
+ }
+}
+
+class SessionHistory {
+ final String id;
+ final String sessionCode;
+ final String quizId;
+ final String hostId;
+ final DateTime createdAt;
+ final DateTime? startedAt;
+ final DateTime? endedAt;
+ final bool isActive;
+ final int participantLimit;
+ final List participants;
+ final int currentQuestionIndex;
+
+ SessionHistory({
+ required this.id,
+ required this.sessionCode,
+ required this.quizId,
+ required this.hostId,
+ required this.createdAt,
+ this.startedAt,
+ this.endedAt,
+ required this.isActive,
+ required this.participantLimit,
+ required this.participants,
+ required this.currentQuestionIndex,
+ });
+
+ factory SessionHistory.fromJson(Map json) {
+ return SessionHistory(
+ id: json['id'],
+ sessionCode: json['session_code'],
+ quizId: json['quiz_id'],
+ hostId: json['host_id'],
+ createdAt: DateTime.parse(json['created_at']),
+ startedAt: json['started_at'] != null ? DateTime.parse(json['started_at']) : null,
+ endedAt: json['ended_at'] != null ? DateTime.parse(json['ended_at']) : null,
+ isActive: json['is_active'],
+ participantLimit: json['participan_limit'], // Typo di JSON, harusnya "participant_limit"
+ participants: (json['participants'] as List).map((p) => Participant.fromJson(p)).toList(),
+ currentQuestionIndex: json['current_question_index'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'session_code': sessionCode,
+ 'quiz_id': quizId,
+ 'host_id': hostId,
+ 'created_at': createdAt.toIso8601String(),
+ 'started_at': startedAt?.toIso8601String(),
+ 'ended_at': endedAt?.toIso8601String(),
+ 'is_active': isActive,
+ 'participan_limit': participantLimit, // Tetap gunakan sesuai field JSON yang ada
+ 'participants': participants.map((p) => p.toJson()).toList(),
+ 'current_question_index': currentQuestionIndex,
+ };
+ }
+}
diff --git a/lib/data/models/login/login_request_model.dart b/lib/data/models/login/login_request_model.dart
new file mode 100644
index 0000000..c54a862
--- /dev/null
+++ b/lib/data/models/login/login_request_model.dart
@@ -0,0 +1,23 @@
+class LoginRequestModel {
+ final String email;
+ final String password;
+
+ LoginRequestModel({
+ required this.email,
+ required this.password,
+ });
+
+ factory LoginRequestModel.fromJson(Map json) {
+ return LoginRequestModel(
+ email: json['email'] ?? '',
+ password: json['password'] ?? '',
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'email': email,
+ 'password': password,
+ };
+ }
+}
diff --git a/lib/data/models/login/login_response_model.dart b/lib/data/models/login/login_response_model.dart
new file mode 100644
index 0000000..df1be0b
--- /dev/null
+++ b/lib/data/models/login/login_response_model.dart
@@ -0,0 +1,55 @@
+class LoginResponseModel {
+ final String? id;
+ final String? googleId;
+ final String email;
+ final String name;
+ final String? birthDate;
+ final String? picUrl;
+ final String? phone;
+ final String locale;
+ final String? createdAt;
+ // final String? updatedAt;
+
+ LoginResponseModel({
+ this.id,
+ this.googleId,
+ required this.email,
+ required this.name,
+ this.birthDate,
+ this.picUrl,
+ this.phone,
+ this.locale = "en-US",
+ this.createdAt,
+ // this.updatedAt,
+ });
+
+ factory LoginResponseModel.fromJson(Map json) {
+ return LoginResponseModel(
+ id: json['id'],
+ googleId: json['google_id'],
+ email: json['email'],
+ name: json['name'],
+ birthDate: json['birth_date'],
+ picUrl: json['pic_url'],
+ phone: json['phone'],
+ locale: json['locale'] ?? 'en-US',
+ createdAt: json['created_at'],
+ // updatedAt: json['updated_at'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'google_id': googleId,
+ 'email': email,
+ 'name': name,
+ 'birth_date': birthDate,
+ 'pic_url': picUrl,
+ 'phone': phone,
+ 'locale': locale,
+ 'created_at': createdAt,
+ // 'updated_at': updatedAt,
+ };
+ }
+}
diff --git a/lib/data/models/quiz/library_quiz_model.dart b/lib/data/models/quiz/library_quiz_model.dart
new file mode 100644
index 0000000..1bae33d
--- /dev/null
+++ b/lib/data/models/quiz/library_quiz_model.dart
@@ -0,0 +1,65 @@
+import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
+
+class QuizData {
+ final String id;
+ final String authorId;
+ final String subjectId;
+ final String subjectName;
+ final String title;
+ final String? description;
+ final bool isPublic;
+ final String? date;
+ final String? time;
+ final int totalQuiz;
+ final int limitDuration;
+ final List questionListings;
+
+ QuizData({
+ required this.id,
+ required this.authorId,
+ required this.subjectId,
+ required this.subjectName,
+ required this.title,
+ this.description,
+ required this.isPublic,
+ this.date,
+ this.time,
+ required this.totalQuiz,
+ required this.limitDuration,
+ required this.questionListings,
+ });
+
+ factory QuizData.fromJson(Map json) {
+ return QuizData(
+ id: json["id"],
+ authorId: json['author_id'],
+ subjectId: json['subject_id'],
+ subjectName: json['subject_alias'],
+ title: json['title'],
+ description: json['description'],
+ isPublic: json['is_public'],
+ date: json['date'],
+ time: json['time'],
+ totalQuiz: json['total_quiz'],
+ limitDuration: json['limit_duration'],
+ questionListings: (json['question_listings'] as List).map((e) => BaseQuestionModel.fromJson(e as Map)).toList(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'author_id': authorId,
+ 'subject_id': subjectId,
+ 'subject_alias': subjectName,
+ 'title': title,
+ 'description': description,
+ 'is_public': isPublic,
+ 'date': date,
+ 'time': time,
+ 'total_quiz': totalQuiz,
+ 'limit_duration': limitDuration,
+ 'question_listings': questionListings.map((e) => e.toJson()).toList(),
+ };
+ }
+}
diff --git a/lib/data/models/quiz/question/base_qustion_model.dart b/lib/data/models/quiz/question/base_qustion_model.dart
new file mode 100644
index 0000000..31843b2
--- /dev/null
+++ b/lib/data/models/quiz/question/base_qustion_model.dart
@@ -0,0 +1,32 @@
+import 'package:quiz_app/data/models/quiz/question/fill_in_the_blank_question_model.dart';
+import 'package:quiz_app/data/models/quiz/question/option_question_model.dart';
+import 'package:quiz_app/data/models/quiz/question/true_false_question_model.dart';
+
+abstract class BaseQuestionModel {
+ final int index;
+ final String question;
+ final int duration;
+ final String type;
+
+ BaseQuestionModel({
+ required this.index,
+ required this.question,
+ required this.duration,
+ required this.type,
+ });
+
+ factory BaseQuestionModel.fromJson(Map json) {
+ switch (json['type']) {
+ case 'fill_the_blank':
+ return FillInTheBlankQuestion.fromJson(json);
+ case 'true_false':
+ return TrueFalseQuestion.fromJson(json);
+ case 'option':
+ return OptionQuestion.fromJson(json);
+ default:
+ throw Exception('Unsupported question type: ${json['type']}');
+ }
+ }
+
+ Map toJson();
+}
diff --git a/lib/data/models/quiz/question/fill_in_the_blank_question_model.dart b/lib/data/models/quiz/question/fill_in_the_blank_question_model.dart
new file mode 100644
index 0000000..b917875
--- /dev/null
+++ b/lib/data/models/quiz/question/fill_in_the_blank_question_model.dart
@@ -0,0 +1,31 @@
+import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
+
+class FillInTheBlankQuestion extends BaseQuestionModel {
+ final String targetAnswer;
+
+ FillInTheBlankQuestion({
+ required super.index,
+ required super.question,
+ required super.duration,
+ required this.targetAnswer,
+ }) : super(type: 'fill_the_blank');
+
+ factory FillInTheBlankQuestion.fromJson(Map json) {
+ return FillInTheBlankQuestion(
+ index: json['index'],
+ question: json['question'],
+ duration: json['duration'],
+ targetAnswer: json['target_answer'],
+ );
+ }
+
+ @override
+ Map toJson() => {
+ 'index': index,
+ 'question': question,
+ 'duration': duration,
+ 'type': type,
+ 'target_answer': targetAnswer,
+ 'options': null,
+ };
+}
diff --git a/lib/data/models/quiz/question/option_question_model.dart b/lib/data/models/quiz/question/option_question_model.dart
new file mode 100644
index 0000000..9b5ce6f
--- /dev/null
+++ b/lib/data/models/quiz/question/option_question_model.dart
@@ -0,0 +1,34 @@
+import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
+
+class OptionQuestion extends BaseQuestionModel {
+ final int targetAnswer;
+ final List options;
+
+ OptionQuestion({
+ required super.index,
+ required super.question,
+ required super.duration,
+ required this.targetAnswer,
+ required this.options,
+ }) : super(type: 'option');
+
+ factory OptionQuestion.fromJson(Map json) {
+ return OptionQuestion(
+ index: json['index'],
+ question: json['question'],
+ duration: json['duration'],
+ targetAnswer: json['target_answer'],
+ options: List.from(json['options']),
+ );
+ }
+
+ @override
+ Map toJson() => {
+ 'index': index,
+ 'question': question,
+ 'duration': duration,
+ 'type': type,
+ 'target_answer': targetAnswer,
+ 'options': options,
+ };
+}
diff --git a/lib/data/models/quiz/question/true_false_question_model.dart b/lib/data/models/quiz/question/true_false_question_model.dart
new file mode 100644
index 0000000..af01464
--- /dev/null
+++ b/lib/data/models/quiz/question/true_false_question_model.dart
@@ -0,0 +1,33 @@
+import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
+
+class TrueFalseQuestion extends BaseQuestionModel {
+ final bool targetAnswer;
+
+ TrueFalseQuestion({
+ required super.index,
+ required super.question,
+ required super.duration,
+ required this.targetAnswer,
+ }) : super(type: 'true_false');
+
+ factory TrueFalseQuestion.fromJson(Map json) {
+ print(json['target_answer']);
+
+ return TrueFalseQuestion(
+ index: json['index'],
+ question: json['question'],
+ duration: json['duration'],
+ targetAnswer: json['target_answer'].toString().toLowerCase() == 'true',
+ );
+ }
+
+ @override
+ Map toJson() => {
+ 'index': index,
+ 'question': question,
+ 'duration': duration,
+ 'type': type,
+ 'target_answer': targetAnswer,
+ 'options': null,
+ };
+}
diff --git a/lib/data/models/quiz/question_create_request.dart b/lib/data/models/quiz/question_create_request.dart
new file mode 100644
index 0000000..a6466ba
--- /dev/null
+++ b/lib/data/models/quiz/question_create_request.dart
@@ -0,0 +1,70 @@
+import 'package:quiz_app/data/models/quiz/question_listings_model.dart';
+
+class QuizCreateRequestModel {
+ final String title;
+ final String description;
+ final bool isPublic;
+ final String date;
+ final int totalQuiz;
+ final int limitDuration;
+ final String authorId;
+ final String subjectId;
+ final List 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.subjectId,
+ required this.questionListings,
+ });
+
+ Map toJson() {
+ return {
+ 'title': title,
+ 'description': description,
+ 'is_public': isPublic,
+ 'date': date,
+ 'total_quiz': totalQuiz,
+ 'limit_duration': limitDuration,
+ 'author_id': authorId,
+ "subject_id": subjectId,
+ 'question_listings': questionListings.map((e) => e.toJson()).toList(),
+ };
+ }
+}
+
+// class QuestionListing {
+// final String question;
+// final String targetAnswer;
+// final int duration;
+// final String type;
+// final List? options;
+
+// QuestionListing({
+// required this.question,
+// required this.targetAnswer,
+// required this.duration,
+// required this.type,
+// this.options,
+// });
+
+// Map toJson() {
+// final map = {
+// 'question': question,
+// 'target_answer': targetAnswer,
+// 'duration': duration,
+// 'type': type,
+// };
+
+// if (options != null && options!.isNotEmpty) {
+// map['options'] = options;
+// }
+
+// return map;
+// }
+// }
diff --git a/lib/data/models/quiz/question_listings_model.dart b/lib/data/models/quiz/question_listings_model.dart
new file mode 100644
index 0000000..32ab571
--- /dev/null
+++ b/lib/data/models/quiz/question_listings_model.dart
@@ -0,0 +1,39 @@
+class QuestionListing {
+ final int index;
+ final String question;
+ final dynamic targetAnswer;
+ final int duration;
+ final String type;
+ final List? options;
+
+ QuestionListing({
+ required this.index,
+ required this.question,
+ required this.targetAnswer,
+ required this.duration,
+ required this.type,
+ this.options,
+ });
+
+ factory QuestionListing.fromJson(Map json) {
+ return QuestionListing(
+ index: json['index'],
+ question: json['question'],
+ targetAnswer: json['target_answer'],
+ duration: json['duration'],
+ type: json['type'],
+ options: json['options'] != null ? List.from(json['options']) : null,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'index': index,
+ 'question': question,
+ 'target_answer': targetAnswer,
+ 'duration': duration,
+ 'type': type,
+ 'options': options,
+ };
+ }
+}
diff --git a/lib/data/models/quiz/quiestion_data_model.dart b/lib/data/models/quiz/quiestion_data_model.dart
new file mode 100644
index 0000000..55d06a7
--- /dev/null
+++ b/lib/data/models/quiz/quiestion_data_model.dart
@@ -0,0 +1,40 @@
+import 'package:quiz_app/app/const/enums/question_type.dart';
+
+class OptionData {
+ final int index;
+ final String text;
+
+ OptionData({required this.index, required this.text});
+}
+
+class QuestionData {
+ final int index;
+ final String? question;
+ final String? answer;
+ final List? options;
+ final int? correctAnswerIndex;
+ final QuestionType? type;
+ final int duration;
+
+ QuestionData({
+ required this.index,
+ this.question,
+ this.answer,
+ this.options,
+ this.correctAnswerIndex,
+ this.duration = 30,
+ this.type,
+ });
+
+ QuestionData copyWith({int? index, String? question, String? answer, List? options, int? correctAnswerIndex, QuestionType? type, int? duration}) {
+ return QuestionData(
+ index: index ?? this.index,
+ question: question ?? this.question,
+ answer: answer ?? this.answer,
+ options: options ?? this.options,
+ correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
+ type: type ?? this.type,
+ duration: duration ?? this.duration,
+ );
+ }
+}
diff --git a/lib/data/models/quiz/quiz_info_model.dart b/lib/data/models/quiz/quiz_info_model.dart
new file mode 100644
index 0000000..5a54afd
--- /dev/null
+++ b/lib/data/models/quiz/quiz_info_model.dart
@@ -0,0 +1,31 @@
+class QuizInfo {
+ final String title;
+ final String description;
+ final int totalQuiz;
+ final int limitDuration;
+
+ QuizInfo({
+ required this.title,
+ required this.description,
+ required this.totalQuiz,
+ required this.limitDuration,
+ });
+
+ factory QuizInfo.fromJson(Map json) {
+ return QuizInfo(
+ title: json['title'],
+ description: json['description'],
+ totalQuiz: json['total_quiz'],
+ limitDuration: json['limit_duration'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'title': title,
+ 'description': description,
+ 'question_count': totalQuiz,
+ 'limit_duration': limitDuration,
+ };
+ }
+}
diff --git a/lib/data/models/quiz/quiz_listing_model.dart b/lib/data/models/quiz/quiz_listing_model.dart
new file mode 100644
index 0000000..3df0073
--- /dev/null
+++ b/lib/data/models/quiz/quiz_listing_model.dart
@@ -0,0 +1,47 @@
+class QuizListingModel {
+ final String quizId;
+ final String authorId;
+ final String authorName;
+ final String title;
+ final String description;
+ final String date;
+ final int totalQuiz;
+ final int duration;
+
+ QuizListingModel({
+ required this.quizId,
+ required this.authorId,
+ required this.authorName,
+ required this.title,
+ required this.description,
+ required this.date,
+ required this.duration,
+ required this.totalQuiz,
+ });
+
+ factory QuizListingModel.fromJson(Map json) {
+ return QuizListingModel(
+ quizId: json['quiz_id'] as String,
+ authorId: json['author_id'] as String,
+ authorName: json['author_name'] as String,
+ title: json['title'] as String,
+ description: json['description'] as String,
+ date: json['date'] as String,
+ duration: json['duration'] as int,
+ totalQuiz: json["total_quiz"] as int,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'quiz_id': quizId,
+ 'author_id': authorId,
+ 'author_name': authorName,
+ 'title': title,
+ 'description': description,
+ 'date': date,
+ 'duration': duration,
+ "total_quiz": totalQuiz
+ };
+ }
+}
diff --git a/lib/data/models/register/register_request.dart b/lib/data/models/register/register_request.dart
new file mode 100644
index 0000000..8059bcc
--- /dev/null
+++ b/lib/data/models/register/register_request.dart
@@ -0,0 +1,25 @@
+class RegisterRequestModel {
+ final String email;
+ final String password;
+ final String name;
+ final String birthDate;
+ final String? phone;
+
+ RegisterRequestModel({
+ required this.email,
+ required this.password,
+ required this.name,
+ required this.birthDate,
+ this.phone,
+ });
+
+ Map toJson() {
+ return {
+ 'email': email,
+ 'password': password,
+ 'name': name,
+ 'birth_date': birthDate,
+ if (phone != null) 'phone': phone,
+ };
+ }
+}
diff --git a/lib/data/models/register/register_response.dart b/lib/data/models/register/register_response.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/data/models/session/session_info_model.dart b/lib/data/models/session/session_info_model.dart
new file mode 100644
index 0000000..db7e3de
--- /dev/null
+++ b/lib/data/models/session/session_info_model.dart
@@ -0,0 +1,64 @@
+import 'package:quiz_app/data/models/user/user_model.dart';
+
+class SessionInfo {
+ final String id;
+ final String sessionCode;
+ final String roomName;
+ final String quizId;
+ final String hostId;
+ final DateTime createdAt;
+ final DateTime? startedAt;
+ final DateTime? endedAt;
+ final bool isActive;
+ final int participantLimit;
+ final List participants;
+ final int currentQuestionIndex;
+
+ SessionInfo({
+ required this.id,
+ required this.sessionCode,
+ required this.roomName,
+ required this.quizId,
+ required this.hostId,
+ required this.createdAt,
+ this.startedAt,
+ this.endedAt,
+ required this.isActive,
+ required this.participantLimit,
+ required this.participants,
+ required this.currentQuestionIndex,
+ });
+
+ factory SessionInfo.fromJson(Map json) {
+ return SessionInfo(
+ id: json['id'],
+ sessionCode: json['session_code'],
+ roomName: json["room_name"],
+ quizId: json['quiz_id'],
+ hostId: json['host_id'],
+ createdAt: DateTime.parse(json['created_at']),
+ startedAt: json['started_at'] != null ? DateTime.parse(json['started_at']) : null,
+ endedAt: json['ended_at'] != null ? DateTime.parse(json['ended_at']) : null,
+ isActive: json['is_active'],
+ participantLimit: json['participan_limit'],
+ participants: (json['participants'] as List?)?.map((e) => UserModel.fromJson(e as Map)).toList() ?? [],
+ currentQuestionIndex: json['current_question_index'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'session_code': sessionCode,
+ 'quiz_id': quizId,
+ 'host_id': hostId,
+ 'created_at': createdAt.toIso8601String(),
+ 'started_at': startedAt?.toIso8601String(),
+ 'ended_at': endedAt?.toIso8601String(),
+ 'is_active': isActive,
+ 'participant_limit': participantLimit,
+ 'participants': participants,
+ 'current_question_index': currentQuestionIndex,
+ };
+ }
+}
diff --git a/lib/data/models/session/session_request_model.dart b/lib/data/models/session/session_request_model.dart
new file mode 100644
index 0000000..2e0dfc8
--- /dev/null
+++ b/lib/data/models/session/session_request_model.dart
@@ -0,0 +1,31 @@
+class SessionRequestModel {
+ final String quizId;
+ final String hostId;
+ final String roomName;
+ final int limitParticipan;
+
+ SessionRequestModel({
+ required this.quizId,
+ required this.hostId,
+ required this.roomName,
+ required this.limitParticipan,
+ });
+
+ factory SessionRequestModel.fromJson(Map json) {
+ return SessionRequestModel(
+ quizId: json['quiz_id'],
+ hostId: json['host_id'],
+ roomName: json['room_name'],
+ limitParticipan: json['limit_participan'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'quiz_id': quizId,
+ 'host_id': hostId,
+ 'room_name': roomName,
+ 'limit_participan': limitParticipan,
+ };
+ }
+}
diff --git a/lib/data/models/session/session_response_model.dart b/lib/data/models/session/session_response_model.dart
new file mode 100644
index 0000000..e9a58e3
--- /dev/null
+++ b/lib/data/models/session/session_response_model.dart
@@ -0,0 +1,20 @@
+class SessionResponseModel {
+ final String sessionId;
+ final String sessionCode;
+
+ SessionResponseModel({required this.sessionId, required this.sessionCode});
+
+ factory SessionResponseModel.fromJson(Map json) {
+ return SessionResponseModel(
+ sessionId: json['session_id'],
+ sessionCode: json['session_code'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'session_id': sessionId,
+ 'session_code': sessionCode,
+ };
+ }
+}
diff --git a/lib/data/models/subject/subject_model.dart b/lib/data/models/subject/subject_model.dart
new file mode 100644
index 0000000..ddc5755
--- /dev/null
+++ b/lib/data/models/subject/subject_model.dart
@@ -0,0 +1,31 @@
+class SubjectModel {
+ final String id;
+ final String name;
+ final String alias;
+ final String description;
+
+ SubjectModel({
+ required this.id,
+ required this.name,
+ required this.alias,
+ required this.description,
+ });
+
+ factory SubjectModel.fromJson(Map json) {
+ return SubjectModel(
+ id: json['id'],
+ name: json['name'],
+ alias: json['alias'],
+ description: json['description'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'alias': alias,
+ 'description': description,
+ };
+ }
+}
diff --git a/lib/data/models/user/user_full_model.dart b/lib/data/models/user/user_full_model.dart
new file mode 100644
index 0000000..46f6ad6
--- /dev/null
+++ b/lib/data/models/user/user_full_model.dart
@@ -0,0 +1,55 @@
+class UserFullModel {
+ final String id;
+ final String googleId;
+ final String email;
+ final String name;
+ final String birthDate;
+ final String picUrl;
+ final String phone;
+ final String locale;
+ final String createdAt;
+ final String updatedAt;
+
+ UserFullModel({
+ required this.id,
+ required this.googleId,
+ required this.email,
+ required this.name,
+ required this.birthDate,
+ required this.picUrl,
+ required this.phone,
+ required this.locale,
+ required this.createdAt,
+ required this.updatedAt,
+ });
+
+ factory UserFullModel.fromJson(Map json) {
+ return UserFullModel(
+ id: json['id'] ?? '',
+ googleId: json['google_id'] ?? '',
+ email: json['email'] ?? '',
+ name: json['name'] ?? '',
+ birthDate: json['birth_date'] ?? '',
+ picUrl: json['pic_url'] ?? '',
+ phone: json['phone'] ?? '',
+ locale: json['locale'] ?? '',
+ createdAt: json['created_at'] ?? '',
+ updatedAt: json['updated_at'] ?? '',
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'google_id': googleId,
+ 'email': email,
+ 'name': name,
+ 'birth_date': birthDate,
+ 'pic_url': picUrl,
+ 'phone': phone,
+ 'locale': locale,
+ 'created_at': createdAt,
+ 'updated_at': updatedAt,
+ };
+ }
+}
diff --git a/lib/data/models/user/user_model.dart b/lib/data/models/user/user_model.dart
new file mode 100644
index 0000000..72769d4
--- /dev/null
+++ b/lib/data/models/user/user_model.dart
@@ -0,0 +1,22 @@
+class UserModel {
+ final String id;
+ final String username;
+ final String userPic;
+ final DateTime joinedAt;
+
+ UserModel({
+ required this.id,
+ required this.username,
+ required this.userPic,
+ required this.joinedAt,
+ });
+
+ factory UserModel.fromJson(Map json) {
+ return UserModel(
+ id: json['id'],
+ username: json['username'],
+ userPic: json['user_pic'] ?? "",
+ joinedAt: DateTime.parse(json['joined_at']),
+ );
+ }
+}
diff --git a/lib/data/models/user/user_stat_model.dart b/lib/data/models/user/user_stat_model.dart
new file mode 100644
index 0000000..769a73f
--- /dev/null
+++ b/lib/data/models/user/user_stat_model.dart
@@ -0,0 +1,29 @@
+class UserStatModel {
+ final double avgScore;
+ final int totalSolve;
+ final int totalQuiz;
+
+ UserStatModel({
+ required this.avgScore,
+ required this.totalSolve,
+ required this.totalQuiz,
+ });
+
+ // Factory constructor to create an instance from JSON
+ factory UserStatModel.fromJson(Map json) {
+ return UserStatModel(
+ avgScore: (json['avg_score'] as num).toDouble(),
+ totalSolve: json['total_solve'] as int,
+ totalQuiz: json['total_quiz'] as int,
+ );
+ }
+
+ // Convert instance to JSON
+ Map toJson() {
+ return {
+ 'avg_score': avgScore,
+ 'total_solve': totalSolve,
+ 'total_quiz': totalQuiz,
+ };
+ }
+}
diff --git a/lib/data/providers/dio_client.dart b/lib/data/providers/dio_client.dart
new file mode 100644
index 0000000..456597d
--- /dev/null
+++ b/lib/data/providers/dio_client.dart
@@ -0,0 +1,52 @@
+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;
+
+ Future init() async {
+ dio = Dio(BaseOptions(
+ baseUrl: APIEndpoint.api,
+ connectTimeout: const Duration(minutes: 3),
+ receiveTimeout: const Duration(minutes: 10),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ ));
+
+ 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;
+ }
+}
diff --git a/lib/data/services/answer_service.dart b/lib/data/services/answer_service.dart
new file mode 100644
index 0000000..79b344a
--- /dev/null
+++ b/lib/data/services/answer_service.dart
@@ -0,0 +1,48 @@
+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';
+import 'package:quiz_app/data/models/base/base_model.dart';
+import 'package:quiz_app/data/models/history/participant_history_result.dart';
+import 'package:quiz_app/data/providers/dio_client.dart';
+
+class AnswerService extends GetxService {
+ late final Dio dio;
+
+ @override
+ void onInit() {
+ dio = Get.find().dio;
+ super.onInit();
+ }
+
+ Future submitQuizAnswers(Map payload) async {
+ try {
+ await dio.post(
+ APIEndpoint.quizAnswer,
+ data: payload,
+ );
+ return BaseResponseModel(message: "success");
+ } on DioException catch (e) {
+ logC.e('Gagal mengirim jawaban: ${e.response?.data['message'] ?? e.message}');
+ return null;
+ }
+ }
+
+ Future?> getAnswerSession(String sessionId, String userId) async {
+ try {
+ final response = await dio.post(APIEndpoint.quizAnswerSession, data: {
+ "session_id": sessionId,
+ "user_id": userId,
+ });
+ final parsedResponse = BaseResponseModel.fromJson(
+ response.data,
+ (data) => ParticipantResult.fromJson(data),
+ );
+
+ return parsedResponse;
+ } on DioException catch (e) {
+ logC.e('Gagal mengirim jawaban: ${e.response?.data['message'] ?? e.message}');
+ return null;
+ }
+ }
+}
diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart
new file mode 100644
index 0000000..cf1de61
--- /dev/null
+++ b/lib/data/services/auth_service.dart
@@ -0,0 +1,74 @@
+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/base/base_model.dart';
+import 'package:quiz_app/data/models/login/login_request_model.dart';
+import 'package:quiz_app/data/models/login/login_response_model.dart';
+import 'package:quiz_app/data/models/register/register_request.dart';
+import 'package:quiz_app/data/providers/dio_client.dart';
+
+class AuthService extends GetxService {
+ late final Dio dio;
+
+ @override
+ void onInit() {
+ dio = Get.find().dio;
+ super.onInit();
+ }
+
+ Future register(RegisterRequestModel request) async {
+ try {
+ final response = await dio.post(
+ APIEndpoint.register,
+ data: request.toJson(),
+ );
+
+ return response.statusCode == 200;
+ } on DioException catch (e) {
+ if (e.response?.statusCode == 409) {
+ // Status 409 = Conflict = User already exists
+ throw Exception("Email sudah dipakai");
+ }
+
+ // Other Dio errors
+ final errorMessage = e.response?.data['message'] ?? "Pendaftaran gagal";
+ throw Exception(errorMessage);
+ } catch (e) {
+ throw Exception("Terjadi kesalahan saat mendaftar");
+ }
+ }
+
+ Future loginWithEmail(LoginRequestModel request) async {
+ try {
+ final data = request.toJson();
+ final response = await dio.post(APIEndpoint.login, data: data);
+
+ final baseResponse = BaseResponseModel.fromJson(
+ response.data,
+ (json) => LoginResponseModel.fromJson(json),
+ );
+ return baseResponse.data!;
+ } on DioException catch (e) {
+ final errorMessage = e.response?.data['message'] ?? "Login gagal";
+ throw Exception(errorMessage);
+ }
+ }
+
+ Future loginWithGoogle(String idToken) async {
+ try {
+ final response = await dio.post(
+ APIEndpoint.loginGoogle,
+ data: {"token_id": idToken},
+ );
+
+ final baseResponse = BaseResponseModel.fromJson(
+ response.data,
+ (json) => LoginResponseModel.fromJson(json),
+ );
+ return baseResponse.data!;
+ } on DioException catch (e) {
+ final errorMessage = e.response?.data['message'] ?? "Login Google gagal";
+ throw Exception(errorMessage);
+ }
+ }
+}
diff --git a/lib/data/services/connection_service.dart b/lib/data/services/connection_service.dart
new file mode 100644
index 0000000..d929023
--- /dev/null
+++ b/lib/data/services/connection_service.dart
@@ -0,0 +1,56 @@
+import 'dart:async';
+import 'package:connectivity_plus/connectivity_plus.dart';
+import 'package:get/get.dart';
+
+/// [ConnectionService] is a GetX Service that monitors internet connectivity status.
+///
+/// It utilizes the [Connectivity] class from the `connectivity_plus` package.
+class ConnectionService extends GetxService {
+ final Connectivity _connectivity = Connectivity();
+
+ /// Subscription to the connectivity change stream.
+ late StreamSubscription> _subscription;
+
+ /// Reactive boolean to indicate the current internet connection status.
+ /// `true` means the device is connected to the internet via Wi-Fi, mobile data, or other means.
+ final RxBool isConnected = true.obs;
+
+ bool get isCurrentlyConnected => isConnected.value;
+
+ /// Called when the service is first initialized.
+ @override
+ void onInit() {
+ super.onInit();
+ _initConnectivity();
+ _subscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
+ }
+
+ /// Checks the initial connectivity status when the service is initialized.
+ Future _initConnectivity() async {
+ try {
+ final result = await _connectivity.checkConnectivity();
+ _updateConnectionStatus(result); // Wrap in a list for consistency
+ } catch (e) {
+ isConnected.value = false;
+ }
+ }
+
+ /// Callback function to handle changes in connectivity status.
+ /// @param results A list of [ConnectivityResult] representing all active network connections.
+ void _updateConnectionStatus(List results) {
+ // If all results are `none`, the device is considered offline.
+ isConnected.value = results.any((result) => result != ConnectivityResult.none);
+ }
+
+ Future isHaveConnection() async {
+ final result = await _connectivity.checkConnectivity();
+ return !result.contains(ConnectivityResult.none);
+ }
+
+ /// Cancels the connectivity subscription when the service is closed.
+ @override
+ void onClose() {
+ _subscription.cancel();
+ super.onClose();
+ }
+}
diff --git a/lib/data/services/google_auth_service.dart b/lib/data/services/google_auth_service.dart
new file mode 100644
index 0000000..9ad6096
--- /dev/null
+++ b/lib/data/services/google_auth_service.dart
@@ -0,0 +1,35 @@
+import 'package:google_sign_in/google_sign_in.dart';
+
+class GoogleAuthService {
+ final GoogleSignIn _googleSignIn = GoogleSignIn(
+ scopes: ['email', 'profile', 'openid'],
+ );
+
+ Future signIn() async {
+ try {
+ return await _googleSignIn.signIn();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future signOut() async {
+ try {
+ await _googleSignIn.signOut();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future getIdToken() async {
+ final account = await _googleSignIn.signIn();
+ if (account == null) return null;
+
+ final auth = await account.authentication;
+ return auth.idToken;
+ }
+
+ Future isSignedIn() async {
+ return await _googleSignIn.isSignedIn();
+ }
+}
diff --git a/lib/data/services/history_service.dart b/lib/data/services/history_service.dart
new file mode 100644
index 0000000..b3816d2
--- /dev/null
+++ b/lib/data/services/history_service.dart
@@ -0,0 +1,65 @@
+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';
+import 'package:quiz_app/data/models/base/base_model.dart';
+import 'package:quiz_app/data/models/history/detail_quiz_history.dart';
+import 'package:quiz_app/data/models/history/quiz_history.dart';
+import 'package:quiz_app/data/models/history/session_history.dart';
+import 'package:quiz_app/data/providers/dio_client.dart';
+
+class HistoryService extends GetxService {
+ late final Dio _dio;
+
+ @override
+ void onInit() {
+ _dio = Get.find().dio;
+ super.onInit();
+ }
+
+ Future?> getHistory(String userId) async {
+ try {
+ final result = await _dio.get("${APIEndpoint.historyQuiz}/$userId");
+
+ final parsedResponse = BaseResponseModel>.fromJson(
+ result.data,
+ (data) => (data as List).map((e) => QuizHistory.fromJson(e as Map)).toList(),
+ );
+ return parsedResponse.data;
+ } catch (e, stacktrace) {
+ logC.e(e, stackTrace: stacktrace);
+ return null;
+ }
+ }
+
+ Future?> getDetailHistory(String answerId) async {
+ try {
+ final result = await _dio.get("${APIEndpoint.detailHistoryQuiz}/$answerId");
+
+ final parsedResponse = BaseResponseModel.fromJson(
+ result.data,
+ (data) => QuizAnswerResult.fromJson(data as Map),
+ );
+ return parsedResponse;
+ } catch (e, stacktrace) {
+ logC.e(e, stackTrace: stacktrace);
+ return null;
+ }
+ }
+
+ Future?> getSessionHistory(String sessionId) async {
+ try {
+ final result = await _dio.get("${APIEndpoint.sessionHistory}/$sessionId");
+
+ final parsedResponse = BaseResponseModel.fromJson(
+ result.data,
+ (data) => SessionHistory.fromJson(data),
+ );
+
+ return parsedResponse;
+ } catch (e, stacktrace) {
+ logC.e(e, stackTrace: stacktrace);
+ return null;
+ }
+ }
+}
diff --git a/lib/data/services/quiz_service.dart b/lib/data/services/quiz_service.dart
new file mode 100644
index 0000000..f62a8e0
--- /dev/null
+++ b/lib/data/services/quiz_service.dart
@@ -0,0 +1,199 @@
+import 'dart:ui';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:quiz_app/core/endpoint/api_endpoint.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/quiz/library_quiz_model.dart';
+import 'package:quiz_app/data/models/quiz/question_create_request.dart';
+import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
+import 'package:quiz_app/data/providers/dio_client.dart';
+
+class QuizService extends GetxService {
+ late final Dio dio;
+
+ @override
+ void onInit() {
+ dio = Get.find().dio;
+ super.onInit();
+ }
+
+ Future 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) {
+ logC.e("Quiz creation error: $e");
+ throw Exception("Quiz creation error: $e");
+ }
+ }
+
+ Future>> createQuizAuto(String sentence) async {
+ try {
+ final response = await dio.post(
+ APIEndpoint.quizGenerate,
+ data: {"sentence": sentence},
+ );
+
+ if (response.statusCode == 200) {
+ print(response.data);
+
+ // Parsing response using BaseResponseModel
+ final parseResponse = BaseResponseModel>.fromJson(
+ response.data,
+ (data) => (data as List).map((item) => RawQuizModel.fromJson(item as Map)).toList(),
+ );
+
+ return parseResponse;
+ } else {
+ throw Exception("Quiz creation failed with status: ${response.statusCode}");
+ }
+ } catch (e) {
+ logC.e("Quiz creation error: $e");
+ throw Exception("Quiz creation error: $e");
+ }
+ }
+
+ Future>?> userQuiz(String userId, int page) async {
+ try {
+ final response = await dio.get("${APIEndpoint.userQuiz}/$userId?page=$page");
+ if (response.statusCode == 200) {
+ final parsedResponse = BaseResponseModel>.fromJson(
+ response.data,
+ (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(),
+ );
+ return parsedResponse;
+ } else {
+ logC.e("Failed to fetch recommendation quizzes. Status: ${response.statusCode}");
+ return null;
+ }
+ } catch (e) {
+ logC.e("Error fetching user quizzes: $e");
+ return null;
+ }
+ }
+
+ Future>?> populerQuiz({int page = 1, int amount = 3}) async {
+ try {
+ Locale locale = Localizations.localeOf(Get.context!);
+ final response = await dio.get("${APIEndpoint.quizPopuler}?page=$page&limit=$amount&lang_code=${locale.languageCode}");
+
+ if (response.statusCode == 200) {
+ final parsedResponse = BaseResponseModel>.fromJson(
+ response.data,
+ (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(),
+ );
+ return parsedResponse;
+ } else {
+ logC.e("Failed to fetch recommendation quizzes. Status: ${response.statusCode}");
+ return null;
+ }
+ } catch (e) {
+ logC.e("Error fetching recommendation quizzes: $e");
+ return null;
+ }
+ }
+
+ Future>?> recommendationQuiz({int page = 1, int amount = 3, String userId = ""}) async {
+ try {
+ Locale locale = Localizations.localeOf(Get.context!);
+ final response = await dio.get("${APIEndpoint.quizRecommendation}?page=$page&limit=$amount&user_id=$userId&lang_code=${locale.languageCode}");
+
+ if (response.statusCode == 200) {
+ final parsedResponse = BaseResponseModel>.fromJson(
+ response.data,
+ (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(),
+ );
+ return parsedResponse;
+ } else {
+ logC.e("Failed to fetch recommendation quizzes. Status: ${response.statusCode}");
+ return null;
+ }
+ } catch (e) {
+ logC.e("Error fetching recommendation quizzes: $e");
+ return null;
+ }
+ }
+
+ Future>?> searchQuiz(String keyword, int page, {int limit = 10, String? subjectId}) async {
+ try {
+ final queryParams = {
+ "keyword": keyword,
+ "page": page.toString(),
+ "limit": limit.toString(),
+ if (subjectId != null && subjectId.isNotEmpty) "subject_id": subjectId,
+ };
+
+ final uri = Uri.parse(APIEndpoint.quizSearch).replace(queryParameters: queryParams);
+ final response = await dio.getUri(uri);
+
+ if (response.statusCode == 200) {
+ final parsedResponse = BaseResponseModel>.fromJson(
+ response.data,
+ (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(),
+ );
+ return parsedResponse;
+ } else {
+ logC.e("Failed to fetch search quizzes. Status: ${response.statusCode}");
+ return null;
+ }
+ } catch (e) {
+ logC.e("Error fetching search quizzes: $e");
+ return null;
+ }
+ }
+
+ Future?> getQuizById(String quizId) async {
+ try {
+ final response = await dio.get("${APIEndpoint.quiz}/$quizId");
+
+ if (response.statusCode == 200) {
+ final parsedResponse = BaseResponseModel.fromJson(
+ response.data,
+ (data) => QuizData.fromJson(data),
+ );
+ return parsedResponse;
+ } else {
+ logC.e("Failed to fetch quiz by id. Status: ${response.statusCode}");
+ return null;
+ }
+ } catch (e, stacktrace) {
+ logC.e("Error fetching quiz by id $e", stackTrace: stacktrace);
+ return null;
+ }
+ }
+}
+
+class RawQuizModel {
+ final String qustion;
+ final dynamic answer;
+
+ RawQuizModel({
+ required this.qustion,
+ required this.answer,
+ });
+
+ factory RawQuizModel.fromJson(Map json) {
+ return RawQuizModel(
+ qustion: json['qustion'] as String,
+ answer: json['answer'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'qustion': qustion,
+ 'answer': answer,
+ };
+ }
+}
diff --git a/lib/data/services/session_service.dart b/lib/data/services/session_service.dart
new file mode 100644
index 0000000..0759009
--- /dev/null
+++ b/lib/data/services/session_service.dart
@@ -0,0 +1,34 @@
+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/base/base_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/providers/dio_client.dart';
+
+class SessionService extends GetxService {
+ late final Dio _dio;
+
+ @override
+ void onInit() {
+ _dio = Get.find().dio;
+ super.onInit();
+ }
+
+ Future?> createSession(SessionRequestModel data) async {
+ try {
+ final response = await _dio.post(APIEndpoint.session, data: data.toJson());
+ if (response.statusCode != 201) {
+ return null;
+ }
+
+ return BaseResponseModel.fromJson(response.data, (e) => SessionResponseModel.fromJson(e));
+ } on DioException catch (e) {
+ print('Error creating session: ${e.response?.data ?? e.message}');
+ return null;
+ } catch (e) {
+ print('Unexpected error: $e');
+ return null;
+ }
+ }
+}
diff --git a/lib/data/services/socket_service.dart b/lib/data/services/socket_service.dart
new file mode 100644
index 0000000..7403c3e
--- /dev/null
+++ b/lib/data/services/socket_service.dart
@@ -0,0 +1,162 @@
+import 'dart:async';
+import 'package:quiz_app/core/endpoint/api_endpoint.dart';
+import 'package:quiz_app/core/utils/logger.dart';
+import 'package:socket_io_client/socket_io_client.dart' as io;
+
+class SocketService {
+ late io.Socket socket;
+
+ final _roomMessageController = StreamController