Compare commits
No commits in common. "cf52ccedec8352be8a09f9e55e5e892a00ff1b39" and "fad52a00c8372ea48879b8984fd13a205365d7e7" have entirely different histories.
cf52ccedec
...
fad52a00c8
|
@ -45,6 +45,4 @@ app.*.map.json
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
# FVM Version Cache
|
# FVM Version Cache
|
||||||
.fvm/
|
.fvm/
|
||||||
|
|
||||||
*.env
|
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
// 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"
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -30,35 +30,12 @@ android {
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
buildTypes {
|
||||||
debug {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig = signingConfigs.debug
|
signingConfig = signingConfigs.debug
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
|
||||||
signingConfig = signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<application
|
<application
|
||||||
android:label="quiz_app"
|
android:label="quiz_app"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
@ -18,12 +17,12 @@
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
|
@ -39,8 +38,8 @@
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="default_web_client_id">
|
|
||||||
730226042143-mv9dlpk9cesirgjh2o0f9hvsk0ks8r2f.apps.googleusercontent.com</string>
|
|
||||||
</resources>
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
|
@ -1,157 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
description: This file stores settings for Dart & Flutter DevTools.
|
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
|
||||||
extensions:
|
|
|
@ -1,24 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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>(UserStorageService());
|
|
||||||
Get.put(ConnectionService());
|
|
||||||
Get.putAsync(() => ApiClient().init());
|
|
||||||
Get.put<UserController>(UserController(Get.find<UserStorageService>()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
enum ListingType { recomendation, populer, subject }
|
|
|
@ -1 +0,0 @@
|
||||||
enum QuestionType { fillTheBlank, option, trueOrFalse }
|
|
|
@ -1,5 +0,0 @@
|
||||||
extension StringCasingExtension on String {
|
|
||||||
String toTitleCase() {
|
|
||||||
return split(' ').map((word) => word.isNotEmpty ? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}' : '').join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
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<GetPage<dynamic>> 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(),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
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";
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class GlobalDropdownField<T> extends StatelessWidget {
|
|
||||||
final T value;
|
|
||||||
final List<DropdownMenuItem<T>> items;
|
|
||||||
final ValueChanged<T?> 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<T>(
|
|
||||||
value: value,
|
|
||||||
isExpanded: true,
|
|
||||||
onChanged: onChanged,
|
|
||||||
items: items,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
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
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
|
||||||
|
|
||||||
class DeleteQuestionDialog {
|
|
||||||
static Future<void> 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"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
|
||||||
|
|
||||||
class AppDialog {
|
|
||||||
static Future<void> 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<void> 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<bool?> showConfirmationDialog(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
required String message,
|
|
||||||
String cancelText = "Batal",
|
|
||||||
String confirmText = "Yakin",
|
|
||||||
Color confirmColor = AppColors.primaryBlue,
|
|
||||||
}) async {
|
|
||||||
return showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: true,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
message,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: AppColors.primaryBlue,
|
|
||||||
side: const BorderSide(color: AppColors.primaryBlue),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
),
|
|
||||||
child: Text(cancelText),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: confirmColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
),
|
|
||||||
child: Text(confirmText),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
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)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,245 +0,0 @@
|
||||||
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<String> _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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
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<String>? 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<String> 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: <answer>"
|
|
||||||
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: <answer>"
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
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<QuizListingModel> 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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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";
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
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;
|
|
|
@ -1,49 +0,0 @@
|
||||||
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<String> userName = "".obs;
|
|
||||||
Rx<String?> userImage = Rx<String?>(null);
|
|
||||||
Rx<String> email = "".obs;
|
|
||||||
|
|
||||||
UserEntity? userData;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
loadUser();
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> 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 = '';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'email': email,
|
|
||||||
'pic_url': picUrl,
|
|
||||||
'birth_date': birthDate,
|
|
||||||
'locale': locale,
|
|
||||||
'phone': phone,
|
|
||||||
"created_at": createdAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return AnswerModel(
|
|
||||||
questionIndex: json['question_index'],
|
|
||||||
answer: json['answer'],
|
|
||||||
isCorrect: json['is_correct'],
|
|
||||||
timeSpent: (json['time_spent'] as num).toDouble(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'question_index': questionIndex,
|
|
||||||
'answer': answer,
|
|
||||||
'is_correct': isCorrect,
|
|
||||||
'time_spent': timeSpent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
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<AnswerModel> answers;
|
|
||||||
|
|
||||||
QuizAnswerSubmissionModel({
|
|
||||||
required this.sessionId,
|
|
||||||
required this.quizId,
|
|
||||||
required this.userId,
|
|
||||||
required this.answeredAt,
|
|
||||||
required this.answers,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory QuizAnswerSubmissionModel.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'session_id': sessionId,
|
|
||||||
'quiz_id': quizId,
|
|
||||||
'user_id': userId,
|
|
||||||
'answered_at': answeredAt.toIso8601String(),
|
|
||||||
'answers': answers.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
class BaseResponseModel<T> {
|
|
||||||
final String message;
|
|
||||||
final T? data;
|
|
||||||
final MetaModel? meta;
|
|
||||||
|
|
||||||
BaseResponseModel({
|
|
||||||
required this.message,
|
|
||||||
this.data,
|
|
||||||
this.meta,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory BaseResponseModel.fromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
T Function(dynamic) fromJsonT,
|
|
||||||
) {
|
|
||||||
return BaseResponseModel<T>(
|
|
||||||
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<String, dynamic> json) {
|
|
||||||
return MetaModel(
|
|
||||||
totalPage: json['total_page'],
|
|
||||||
currentPage: json['current_page'],
|
|
||||||
totalData: json['total_data'],
|
|
||||||
totalAllData: json['total_all_data'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'total_page': totalPage,
|
|
||||||
'current_page': currentPage,
|
|
||||||
'total_data': totalData,
|
|
||||||
'total_all_data': totalAllData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
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<QuestionAnswerItem> 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<String, dynamic> 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<String>? 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<String, dynamic> 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<String>.from(json['options']) : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
class QuestionAnswer {
|
|
||||||
final int index;
|
|
||||||
final String question;
|
|
||||||
final dynamic targetAnswer;
|
|
||||||
final int duration;
|
|
||||||
final String type;
|
|
||||||
final List<String>? 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<String, dynamic> json) {
|
|
||||||
return QuestionAnswer(
|
|
||||||
index: json['index'],
|
|
||||||
question: json['question'],
|
|
||||||
targetAnswer: json['target_answer'],
|
|
||||||
duration: json['duration'],
|
|
||||||
type: json['type'],
|
|
||||||
options: json['options'] != null ? List<String>.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<QuestionAnswer> 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<String, dynamic> 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";
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'quiz_id': quizId,
|
|
||||||
'answer_id': answerId,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'total_correct': totalCorrect,
|
|
||||||
'total_question': totalQuestion,
|
|
||||||
'date': date
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return Participant(
|
|
||||||
id: json['id'],
|
|
||||||
name: json['name'],
|
|
||||||
score: json['score'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> 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<Participant> 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<String, dynamic> 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<String, dynamic> 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
class LoginRequestModel {
|
|
||||||
final String email;
|
|
||||||
final String password;
|
|
||||||
|
|
||||||
LoginRequestModel({
|
|
||||||
required this.email,
|
|
||||||
required this.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory LoginRequestModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return LoginRequestModel(
|
|
||||||
email: json['email'] ?? '',
|
|
||||||
password: json['password'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'email': email,
|
|
||||||
'password': password,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
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<BaseQuestionModel> 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<String, dynamic> 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<String, dynamic>)).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> 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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> toJson();
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return FillInTheBlankQuestion(
|
|
||||||
index: json['index'],
|
|
||||||
question: json['question'],
|
|
||||||
duration: json['duration'],
|
|
||||||
targetAnswer: json['target_answer'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'index': index,
|
|
||||||
'question': question,
|
|
||||||
'duration': duration,
|
|
||||||
'type': type,
|
|
||||||
'target_answer': targetAnswer,
|
|
||||||
'options': null,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
|
|
||||||
|
|
||||||
class OptionQuestion extends BaseQuestionModel {
|
|
||||||
final int targetAnswer;
|
|
||||||
final List<String> options;
|
|
||||||
|
|
||||||
OptionQuestion({
|
|
||||||
required super.index,
|
|
||||||
required super.question,
|
|
||||||
required super.duration,
|
|
||||||
required this.targetAnswer,
|
|
||||||
required this.options,
|
|
||||||
}) : super(type: 'option');
|
|
||||||
|
|
||||||
factory OptionQuestion.fromJson(Map<String, dynamic> json) {
|
|
||||||
return OptionQuestion(
|
|
||||||
index: json['index'],
|
|
||||||
question: json['question'],
|
|
||||||
duration: json['duration'],
|
|
||||||
targetAnswer: json['target_answer'],
|
|
||||||
options: List<String>.from(json['options']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'index': index,
|
|
||||||
'question': question,
|
|
||||||
'duration': duration,
|
|
||||||
'type': type,
|
|
||||||
'target_answer': targetAnswer,
|
|
||||||
'options': options,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> toJson() => {
|
|
||||||
'index': index,
|
|
||||||
'question': question,
|
|
||||||
'duration': duration,
|
|
||||||
'type': type,
|
|
||||||
'target_answer': targetAnswer,
|
|
||||||
'options': null,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
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<QuestionListing> questionListings;
|
|
||||||
|
|
||||||
QuizCreateRequestModel({
|
|
||||||
required this.title,
|
|
||||||
required this.description,
|
|
||||||
required this.isPublic,
|
|
||||||
required this.date,
|
|
||||||
required this.totalQuiz,
|
|
||||||
required this.limitDuration,
|
|
||||||
required this.authorId,
|
|
||||||
required this.subjectId,
|
|
||||||
required this.questionListings,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'is_public': isPublic,
|
|
||||||
'date': date,
|
|
||||||
'total_quiz': totalQuiz,
|
|
||||||
'limit_duration': limitDuration,
|
|
||||||
'author_id': authorId,
|
|
||||||
"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<String>? options;
|
|
||||||
|
|
||||||
// QuestionListing({
|
|
||||||
// required this.question,
|
|
||||||
// required this.targetAnswer,
|
|
||||||
// required this.duration,
|
|
||||||
// required this.type,
|
|
||||||
// this.options,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Map<String, dynamic> toJson() {
|
|
||||||
// final map = <String, dynamic>{
|
|
||||||
// 'question': question,
|
|
||||||
// 'target_answer': targetAnswer,
|
|
||||||
// 'duration': duration,
|
|
||||||
// 'type': type,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (options != null && options!.isNotEmpty) {
|
|
||||||
// map['options'] = options;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return map;
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,39 +0,0 @@
|
||||||
class QuestionListing {
|
|
||||||
final int index;
|
|
||||||
final String question;
|
|
||||||
final dynamic targetAnswer;
|
|
||||||
final int duration;
|
|
||||||
final String type;
|
|
||||||
final List<String>? options;
|
|
||||||
|
|
||||||
QuestionListing({
|
|
||||||
required this.index,
|
|
||||||
required this.question,
|
|
||||||
required this.targetAnswer,
|
|
||||||
required this.duration,
|
|
||||||
required this.type,
|
|
||||||
this.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory QuestionListing.fromJson(Map<String, dynamic> json) {
|
|
||||||
return QuestionListing(
|
|
||||||
index: json['index'],
|
|
||||||
question: json['question'],
|
|
||||||
targetAnswer: json['target_answer'],
|
|
||||||
duration: json['duration'],
|
|
||||||
type: json['type'],
|
|
||||||
options: json['options'] != null ? List<String>.from(json['options']) : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'index': index,
|
|
||||||
'question': question,
|
|
||||||
'target_answer': targetAnswer,
|
|
||||||
'duration': duration,
|
|
||||||
'type': type,
|
|
||||||
'options': options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
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<OptionData>? 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<OptionData>? 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return QuizInfo(
|
|
||||||
title: json['title'],
|
|
||||||
description: json['description'],
|
|
||||||
totalQuiz: json['total_quiz'],
|
|
||||||
limitDuration: json['limit_duration'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'question_count': totalQuiz,
|
|
||||||
'limit_duration': limitDuration,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'quiz_id': quizId,
|
|
||||||
'author_id': authorId,
|
|
||||||
'author_name': authorName,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'date': date,
|
|
||||||
'duration': duration,
|
|
||||||
"total_quiz": totalQuiz
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
class RegisterRequestModel {
|
|
||||||
final String email;
|
|
||||||
final String password;
|
|
||||||
final String name;
|
|
||||||
final String birthDate;
|
|
||||||
final String? phone;
|
|
||||||
|
|
||||||
RegisterRequestModel({
|
|
||||||
required this.email,
|
|
||||||
required this.password,
|
|
||||||
required this.name,
|
|
||||||
required this.birthDate,
|
|
||||||
this.phone,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'email': email,
|
|
||||||
'password': password,
|
|
||||||
'name': name,
|
|
||||||
'birth_date': birthDate,
|
|
||||||
if (phone != null) 'phone': phone,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
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<UserModel> 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<String, dynamic> 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<dynamic>?)?.map((e) => UserModel.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
|
||||||
currentQuestionIndex: json['current_question_index'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return SessionRequestModel(
|
|
||||||
quizId: json['quiz_id'],
|
|
||||||
hostId: json['host_id'],
|
|
||||||
roomName: json['room_name'],
|
|
||||||
limitParticipan: json['limit_participan'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'quiz_id': quizId,
|
|
||||||
'host_id': hostId,
|
|
||||||
'room_name': roomName,
|
|
||||||
'limit_participan': limitParticipan,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
class SessionResponseModel {
|
|
||||||
final String sessionId;
|
|
||||||
final String sessionCode;
|
|
||||||
|
|
||||||
SessionResponseModel({required this.sessionId, required this.sessionCode});
|
|
||||||
|
|
||||||
factory SessionResponseModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return SessionResponseModel(
|
|
||||||
sessionId: json['session_id'],
|
|
||||||
sessionCode: json['session_code'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'session_id': sessionId,
|
|
||||||
'session_code': sessionCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return SubjectModel(
|
|
||||||
id: json['id'],
|
|
||||||
name: json['name'],
|
|
||||||
alias: json['alias'],
|
|
||||||
description: json['description'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'alias': alias,
|
|
||||||
'description': description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
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<String, dynamic> json) {
|
|
||||||
return UserModel(
|
|
||||||
id: json['id'],
|
|
||||||
username: json['username'],
|
|
||||||
userPic: json['user_pic'] ?? "",
|
|
||||||
joinedAt: DateTime.parse(json['joined_at']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
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<String, dynamic> 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<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'avg_score': avgScore,
|
|
||||||
'total_solve': totalSolve,
|
|
||||||
'total_quiz': totalQuiz,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
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<ApiClient> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
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<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel?> submitQuizAnswers(Map<String, dynamic> 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<BaseResponseModel<ParticipantResult>?> getAnswerSession(String sessionId, String userId) async {
|
|
||||||
try {
|
|
||||||
final response = await dio.post(APIEndpoint.quizAnswerSession, data: {
|
|
||||||
"session_id": sessionId,
|
|
||||||
"user_id": userId,
|
|
||||||
});
|
|
||||||
final parsedResponse = BaseResponseModel<ParticipantResult>.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
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<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> 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<LoginResponseModel> loginWithEmail(LoginRequestModel request) async {
|
|
||||||
try {
|
|
||||||
final data = request.toJson();
|
|
||||||
final response = await dio.post(APIEndpoint.login, data: data);
|
|
||||||
|
|
||||||
final baseResponse = BaseResponseModel<LoginResponseModel>.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<LoginResponseModel> loginWithGoogle(String idToken) async {
|
|
||||||
try {
|
|
||||||
final response = await dio.post(
|
|
||||||
APIEndpoint.loginGoogle,
|
|
||||||
data: {"token_id": idToken},
|
|
||||||
);
|
|
||||||
|
|
||||||
final baseResponse = BaseResponseModel<LoginResponseModel>.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
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<List<ConnectivityResult>> _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<void> _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<ConnectivityResult> results) {
|
|
||||||
// If all results are `none`, the device is considered offline.
|
|
||||||
isConnected.value = results.any((result) => result != ConnectivityResult.none);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import 'package:google_sign_in/google_sign_in.dart';
|
|
||||||
|
|
||||||
class GoogleAuthService {
|
|
||||||
final GoogleSignIn _googleSignIn = GoogleSignIn(
|
|
||||||
scopes: ['email', 'profile', 'openid'],
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<GoogleSignInAccount?> signIn() async {
|
|
||||||
try {
|
|
||||||
return await _googleSignIn.signIn();
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> signOut() async {
|
|
||||||
try {
|
|
||||||
await _googleSignIn.signOut();
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> getIdToken() async {
|
|
||||||
final account = await _googleSignIn.signIn();
|
|
||||||
if (account == null) return null;
|
|
||||||
|
|
||||||
final auth = await account.authentication;
|
|
||||||
return auth.idToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isSignedIn() async {
|
|
||||||
return await _googleSignIn.isSignedIn();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
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<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<QuizHistory>?> getHistory(String userId) async {
|
|
||||||
try {
|
|
||||||
final result = await _dio.get("${APIEndpoint.historyQuiz}/$userId");
|
|
||||||
|
|
||||||
final parsedResponse = BaseResponseModel<List<QuizHistory>>.fromJson(
|
|
||||||
result.data,
|
|
||||||
(data) => (data as List).map((e) => QuizHistory.fromJson(e as Map<String, dynamic>)).toList(),
|
|
||||||
);
|
|
||||||
return parsedResponse.data;
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logC.e(e, stackTrace: stacktrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<QuizAnswerResult>?> getDetailHistory(String answerId) async {
|
|
||||||
try {
|
|
||||||
final result = await _dio.get("${APIEndpoint.detailHistoryQuiz}/$answerId");
|
|
||||||
|
|
||||||
final parsedResponse = BaseResponseModel<QuizAnswerResult>.fromJson(
|
|
||||||
result.data,
|
|
||||||
(data) => QuizAnswerResult.fromJson(data as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
return parsedResponse;
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logC.e(e, stackTrace: stacktrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<SessionHistory>?> getSessionHistory(String sessionId) async {
|
|
||||||
try {
|
|
||||||
final result = await _dio.get("${APIEndpoint.sessionHistory}/$sessionId");
|
|
||||||
|
|
||||||
final parsedResponse = BaseResponseModel<SessionHistory>.fromJson(
|
|
||||||
result.data,
|
|
||||||
(data) => SessionHistory.fromJson(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResponse;
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logC.e(e, stackTrace: stacktrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,199 +0,0 @@
|
||||||
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<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> createQuiz(QuizCreateRequestModel request) async {
|
|
||||||
try {
|
|
||||||
final response = await dio.post(
|
|
||||||
APIEndpoint.quiz,
|
|
||||||
data: request.toJson(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 201) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw Exception("Quiz creation failed");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logC.e("Quiz creation error: $e");
|
|
||||||
throw Exception("Quiz creation error: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<List<RawQuizModel>>> 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<List<RawQuizModel>>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => (data as List).map((item) => RawQuizModel.fromJson(item as Map<String, dynamic>)).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<BaseResponseModel<List<QuizListingModel>>?> userQuiz(String userId, int page) async {
|
|
||||||
try {
|
|
||||||
final response = await dio.get("${APIEndpoint.userQuiz}/$userId?page=$page");
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final parsedResponse = BaseResponseModel<List<QuizListingModel>>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map<String, dynamic>)).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<BaseResponseModel<List<QuizListingModel>>?> 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<List<QuizListingModel>>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map<String, dynamic>)).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<BaseResponseModel<List<QuizListingModel>>?> 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<List<QuizListingModel>>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map<String, dynamic>)).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<BaseResponseModel<List<QuizListingModel>>?> 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<List<QuizListingModel>>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map<String, dynamic>)).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<BaseResponseModel<QuizData>?> getQuizById(String quizId) async {
|
|
||||||
try {
|
|
||||||
final response = await dio.get("${APIEndpoint.quiz}/$quizId");
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final parsedResponse = BaseResponseModel<QuizData>.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<String, dynamic> json) {
|
|
||||||
return RawQuizModel(
|
|
||||||
qustion: json['qustion'] as String,
|
|
||||||
answer: json['answer'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'qustion': qustion,
|
|
||||||
'answer': answer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
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<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<SessionResponseModel>?> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
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<Map<String, dynamic>>.broadcast();
|
|
||||||
final _chatMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
|
||||||
final _questionUpdateController = StreamController<Map<String, dynamic>>.broadcast();
|
|
||||||
final _quizStartedController = StreamController<void>.broadcast();
|
|
||||||
final _answerSubmittedController = StreamController<Map<String, dynamic>>.broadcast();
|
|
||||||
final _scoreUpdateController = StreamController<Map<String, dynamic>>.broadcast();
|
|
||||||
final _quizDoneController = StreamController<void>.broadcast();
|
|
||||||
final _roomClosedController = StreamController<String>.broadcast();
|
|
||||||
final _errorController = StreamController<String>.broadcast();
|
|
||||||
|
|
||||||
// Public streams
|
|
||||||
Stream<Map<String, dynamic>> get roomMessages => _roomMessageController.stream;
|
|
||||||
Stream<Map<String, dynamic>> get questionUpdate => _questionUpdateController.stream;
|
|
||||||
Stream<Map<String, dynamic>> get chatMessages => _chatMessageController.stream;
|
|
||||||
Stream<void> get quizStarted => _quizStartedController.stream;
|
|
||||||
Stream<Map<String, dynamic>> get answerSubmitted => _answerSubmittedController.stream;
|
|
||||||
Stream<Map<String, dynamic>> get scoreUpdates => _scoreUpdateController.stream;
|
|
||||||
Stream<void> get quizDone => _quizDoneController.stream;
|
|
||||||
Stream<String> get roomClosed => _roomClosedController.stream;
|
|
||||||
Stream<String> get errors => _errorController.stream;
|
|
||||||
|
|
||||||
void initSocketConnection() {
|
|
||||||
socket = io.io(
|
|
||||||
APIEndpoint.baseUrl,
|
|
||||||
io.OptionBuilder().setTransports(['websocket']).disableAutoConnect().build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
socket.connect();
|
|
||||||
|
|
||||||
socket.onConnect((_) {
|
|
||||||
logC.i('✅ Connected: ${socket.id}');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.onDisconnect((_) {
|
|
||||||
logC.i('❌ Disconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connection_response', (data) {
|
|
||||||
logC.i('🟢 Connection response: $data');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('room_message', (data) {
|
|
||||||
logC.i('📥 Room Message: $data');
|
|
||||||
_roomMessageController.add(Map<String, dynamic>.from(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('receive_message', (data) {
|
|
||||||
logC.i('💬 Chat from ${data['from']}: ${data['message']}');
|
|
||||||
_chatMessageController.add(Map<String, dynamic>.from(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('quiz_started', (_) {
|
|
||||||
logC.i('🚀 Quiz Started!');
|
|
||||||
_quizStartedController.add(null);
|
|
||||||
});
|
|
||||||
socket.on('quiz_question', (data) {
|
|
||||||
logC.i('🚀 question getted!');
|
|
||||||
_questionUpdateController.add(Map<String, dynamic>.from(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('answer_submitted', (data) {
|
|
||||||
logC.i('✅ Answer Submitted: $data');
|
|
||||||
_answerSubmittedController.add(Map<String, dynamic>.from(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('score_update', (data) {
|
|
||||||
logC.i('📊 Score Update: $data');
|
|
||||||
_scoreUpdateController.add(Map<String, dynamic>.from(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('quiz_done', (_) {
|
|
||||||
logC.i('🏁 Quiz Finished!');
|
|
||||||
_quizDoneController.add(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('room_closed', (data) {
|
|
||||||
logC.i('🔒 Room Closed: $data');
|
|
||||||
_roomClosedController.add(data['room'].toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (data) {
|
|
||||||
logC.e('⚠️ Socket Error: $data');
|
|
||||||
_errorController.add(data.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void joinRoom({required String sessionCode, required String userId}) {
|
|
||||||
socket.emit('join_room', {
|
|
||||||
'session_code': sessionCode,
|
|
||||||
'user_id': userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void leaveRoom({required String sessionId, required String userId, String username = "anonymous"}) {
|
|
||||||
socket.emit('leave_room', {
|
|
||||||
'session_id': sessionId,
|
|
||||||
'user_id': userId,
|
|
||||||
'username': username,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendMessage({
|
|
||||||
required String sessionId,
|
|
||||||
required String message,
|
|
||||||
String username = "anonymous",
|
|
||||||
}) {
|
|
||||||
socket.emit('send_message', {
|
|
||||||
'session_id': sessionId,
|
|
||||||
'message': message,
|
|
||||||
'username': username,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void startQuiz({required String sessionId}) {
|
|
||||||
socket.emit('start_quiz', {
|
|
||||||
'session_id': sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendAnswer({
|
|
||||||
required String sessionId,
|
|
||||||
required String userId,
|
|
||||||
required int questionIndex,
|
|
||||||
required int timeSpent,
|
|
||||||
required dynamic answer,
|
|
||||||
}) {
|
|
||||||
socket.emit('submit_answer', {
|
|
||||||
'session_id': sessionId,
|
|
||||||
'user_id': userId,
|
|
||||||
'question_index': questionIndex,
|
|
||||||
'answer': answer,
|
|
||||||
'time_spent': timeSpent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void endSession({required String sessionId, required String userId}) {
|
|
||||||
socket.emit('end_session', {
|
|
||||||
'session_id': sessionId,
|
|
||||||
'user_id': userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
socket.dispose();
|
|
||||||
_roomMessageController.close();
|
|
||||||
_chatMessageController.close();
|
|
||||||
_quizStartedController.close();
|
|
||||||
_answerSubmittedController.close();
|
|
||||||
_scoreUpdateController.close();
|
|
||||||
_quizDoneController.close();
|
|
||||||
_roomClosedController.close();
|
|
||||||
_errorController.close();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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/subject/subject_model.dart';
|
|
||||||
import 'package:quiz_app/data/providers/dio_client.dart';
|
|
||||||
|
|
||||||
class SubjectService extends GetxService {
|
|
||||||
late final Dio dio;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
dio = Get.find<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<List<SubjectModel>>> getSubject() async {
|
|
||||||
final response = await dio.get(APIEndpoint.subject);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final parsedResponse = BaseResponseModel<List<SubjectModel>>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => (data as List).map((e) => SubjectModel.fromJson(e as Map<String, dynamic>)).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResponse;
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to fetch subjects. Status code: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
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/user/user_full_model.dart';
|
|
||||||
import 'package:quiz_app/data/models/user/user_stat_model.dart';
|
|
||||||
import 'package:quiz_app/data/providers/dio_client.dart';
|
|
||||||
|
|
||||||
class UserService extends GetxService {
|
|
||||||
late final Dio _dio;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
_dio = Get.find<ApiClient>().dio;
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> updateProfileData(String id, String name, {String? birthDate, String? locale, String? phone}) async {
|
|
||||||
try {
|
|
||||||
final response = await _dio.post(APIEndpoint.userUpdate, data: {
|
|
||||||
"id": id,
|
|
||||||
"name": name,
|
|
||||||
"birth_date": birthDate,
|
|
||||||
"locale": locale,
|
|
||||||
"phone": phone,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logC.e("update profile error: $e");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<UserFullModel>?> getUserData(String id) async {
|
|
||||||
try {
|
|
||||||
final response = await _dio.get("${APIEndpoint.userData}/$id");
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final parsedResponse = BaseResponseModel<UserFullModel>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => UserFullModel.fromJson(data),
|
|
||||||
);
|
|
||||||
return parsedResponse;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logC.e("get user data error: $e");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<BaseResponseModel<UserStatModel>?> getUserStat(String id) async {
|
|
||||||
try {
|
|
||||||
final response = await _dio.get("${APIEndpoint.userStat}/$id");
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final parsedResponse = BaseResponseModel<UserStatModel>.fromJson(
|
|
||||||
response.data,
|
|
||||||
(data) => UserStatModel.fromJson(data),
|
|
||||||
);
|
|
||||||
return parsedResponse;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logC.e("get user data error: $e");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:quiz_app/data/entity/user/user_entity.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
/// A lightweight wrapper around [SharedPreferences] that persists
|
|
||||||
/// the logged‑in user plus UI/feature preferences such as theme and
|
|
||||||
/// push‑notification opt‑in.
|
|
||||||
class UserStorageService {
|
|
||||||
// ───────────────────── Keys ─────────────────────
|
|
||||||
static const _userKey = 'user_data';
|
|
||||||
static const _darkModeKey = 'pref_dark_mode';
|
|
||||||
static const _pushNotifKey = 'pref_push_notification';
|
|
||||||
|
|
||||||
/// Cached flag used by splash / root to decide initial route.
|
|
||||||
bool isLogged = false;
|
|
||||||
|
|
||||||
// ───────────────────── User CRUD ─────────────────────
|
|
||||||
Future<void> saveUser(UserEntity user) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setString(_userKey, jsonEncode(user.toJson()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<UserEntity?> loadUser() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final jsonString = prefs.getString(_userKey);
|
|
||||||
if (jsonString == null) return null;
|
|
||||||
return UserEntity.fromJson(jsonDecode(jsonString));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clearUser() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.remove(_userKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isLoggedIn() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
return prefs.containsKey(_userKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ───────────────────── UI Preferences ─────────────────────
|
|
||||||
/// Persist the user’s theme choice.
|
|
||||||
Future<void> setDarkMode(bool value) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setBool(_darkModeKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve the stored theme choice. Defaults to *false* (light mode).
|
|
||||||
Future<bool> getDarkMode() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
return prefs.getBool(_darkModeKey) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Persist the user’s push‑notification preference.
|
|
||||||
Future<void> setPushNotification(bool value) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setBool(_pushNotifKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve the stored push‑notification preference. Defaults to *true*.
|
|
||||||
Future<bool> getPushNotification() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
return prefs.getBool(_pushNotifKey) ?? true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/data/services/history_service.dart';
|
|
||||||
import 'package:quiz_app/feature/admin_result_page/controller/admin_result_controller.dart';
|
|
||||||
|
|
||||||
class AdminResultBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut(() => HistoryService());
|
|
||||||
Get.lazyPut(() => AdminResultController(Get.find<HistoryService>()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/data/services/answer_service.dart';
|
|
||||||
import 'package:quiz_app/feature/admin_result_page/controller/detail_participant_result_controller.dart';
|
|
||||||
|
|
||||||
class DetailParticipantResultBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut(() => AnswerService());
|
|
||||||
Get.lazyPut(() => ParticipantResultController(Get.find<AnswerService>()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/routes/app_pages.dart';
|
|
||||||
import 'package:quiz_app/data/models/history/session_history.dart';
|
|
||||||
import 'package:quiz_app/data/services/history_service.dart';
|
|
||||||
|
|
||||||
class AdminResultController extends GetxController {
|
|
||||||
final HistoryService _historyService;
|
|
||||||
|
|
||||||
AdminResultController(this._historyService);
|
|
||||||
|
|
||||||
SessionHistory? sessionHistory;
|
|
||||||
RxBool isLoading = false.obs;
|
|
||||||
|
|
||||||
String sessionId = "";
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
loadData();
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadData() async {
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
sessionId = Get.arguments as String;
|
|
||||||
final result = await _historyService.getSessionHistory(sessionId);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
sessionHistory = result.data!;
|
|
||||||
print(sessionHistory!.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void goToDetailParticipants(String userId, String username) => Get.toNamed(
|
|
||||||
AppRoutes.quizMPLResultPage,
|
|
||||||
arguments: {
|
|
||||||
"user_id": userId,
|
|
||||||
"session_id": sessionId,
|
|
||||||
"username": username,
|
|
||||||
"is_admin": true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/routes/app_pages.dart';
|
|
||||||
import 'package:quiz_app/data/models/history/participant_history_result.dart';
|
|
||||||
import 'package:quiz_app/data/services/answer_service.dart';
|
|
||||||
|
|
||||||
class ParticipantResultController extends GetxController {
|
|
||||||
final AnswerService _answerService;
|
|
||||||
|
|
||||||
ParticipantResultController(this._answerService);
|
|
||||||
|
|
||||||
final Rx<ParticipantResult?> participantResult = Rx<ParticipantResult?>(null);
|
|
||||||
final RxBool isLoading = false.obs;
|
|
||||||
|
|
||||||
RxString participantName = "".obs;
|
|
||||||
bool isAdmin = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
loadData();
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadData() async {
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final args = Get.arguments;
|
|
||||||
participantName.value = args["username"];
|
|
||||||
isAdmin = args["is_admin"];
|
|
||||||
final response = await _answerService.getAnswerSession(args["session_id"], args["user_id"]);
|
|
||||||
|
|
||||||
if (response != null) {
|
|
||||||
participantResult.value = response.data;
|
|
||||||
}
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
double calculateScorePercent() {
|
|
||||||
if (participantResult.value == null) return 0;
|
|
||||||
return participantResult.value!.scorePercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getTotalCorrect() {
|
|
||||||
return participantResult.value?.totalCorrect ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getTotalQuestions() {
|
|
||||||
return participantResult.value?.totalQuestions ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void goBackPage() {
|
|
||||||
if (isAdmin) {
|
|
||||||
Get.back();
|
|
||||||
} else {
|
|
||||||
Get.offAllNamed(AppRoutes.mainPage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPop(bool isPop, dynamic value) {
|
|
||||||
if (isAdmin) {
|
|
||||||
Get.back();
|
|
||||||
} else {
|
|
||||||
Get.offAllNamed(AppRoutes.mainPage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
|
||||||
import 'package:get/get_state_manager/src/simple/get_view.dart';
|
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
|
||||||
import 'package:quiz_app/app/const/text/text_style.dart';
|
|
||||||
import 'package:quiz_app/data/models/history/session_history.dart';
|
|
||||||
import 'package:quiz_app/feature/admin_result_page/controller/admin_result_controller.dart';
|
|
||||||
|
|
||||||
class AdminResultPage extends GetView<AdminResultController> {
|
|
||||||
const AdminResultPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
body: SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.sessionHistory == null) {
|
|
||||||
return const Center(child: Text("Data tidak ditemukan."));
|
|
||||||
}
|
|
||||||
|
|
||||||
final participants = controller.sessionHistory!.participants;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildSectionHeader("Hasil Akhir Kuis"),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildSummaryCard(participants),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildSectionHeader('Peringkat Peserta'),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemCount: participants.length,
|
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 10),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final participant = participants[index];
|
|
||||||
return _buildParticipantResultCard(
|
|
||||||
participant,
|
|
||||||
position: index + 1,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(title, style: AppTextStyles.title),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSummaryCard(List<Participant> participants) {
|
|
||||||
final avgScore = participants.isNotEmpty ? participants.map((p) => p.score).reduce((a, b) => a + b) / participants.length : 0.0;
|
|
||||||
|
|
||||||
final passCount = participants.where((p) => p.score >= 60).length;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
shadowColor: AppColors.shadowPrimary.withOpacity(0.2),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: BorderSide(color: AppColors.accentBlue.withOpacity(0.2)),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.assignment_turned_in, color: AppColors.primaryBlue, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
"RINGKASAN KUIS",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.primaryBlue,
|
|
||||||
letterSpacing: 0.8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
_buildSummaryItem(
|
|
||||||
icon: Icons.group,
|
|
||||||
value: "${participants.length}",
|
|
||||||
label: "Total Peserta",
|
|
||||||
),
|
|
||||||
_buildSummaryItem(
|
|
||||||
icon: Icons.percent,
|
|
||||||
value: "${avgScore.toStringAsFixed(1)}%",
|
|
||||||
label: "Rata-Rata Nilai",
|
|
||||||
valueColor: _getScoreColor(avgScore),
|
|
||||||
),
|
|
||||||
_buildSummaryItem(
|
|
||||||
icon: Icons.emoji_events,
|
|
||||||
value: "$passCount/${participants.length}",
|
|
||||||
label: "Peserta Lulus",
|
|
||||||
valueColor: AppColors.scoreGood,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSummaryItem({
|
|
||||||
required IconData icon,
|
|
||||||
required String value,
|
|
||||||
required String label,
|
|
||||||
Color? valueColor,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: AppColors.softGrayText, size: 22),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: valueColor ?? AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: AppTextStyles.caption,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildParticipantResultCard(Participant participant, {required int position}) {
|
|
||||||
final scorePercent = participant.score.toDouble();
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
shadowColor: AppColors.shadowPrimary.withOpacity(0.2),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: BorderSide(color: AppColors.accentBlue.withOpacity(0.2)),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => controller.goToDetailParticipants(participant.id, participant.name),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: _getPositionColor(position),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
position.toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(participant.name, style: AppTextStyles.subtitle),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text("Skor: ${participant.score}", style: AppTextStyles.caption),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: _getScoreColor(scorePercent).withOpacity(0.1),
|
|
||||||
border: Border.all(
|
|
||||||
color: _getScoreColor(scorePercent),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
"${participant.score}%",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _getScoreColor(scorePercent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Icon(Icons.chevron_right, color: AppColors.softGrayText, size: 20),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getScoreColor(double score) {
|
|
||||||
if (score >= 80) return AppColors.scoreExcellent;
|
|
||||||
if (score >= 70) return AppColors.scoreGood;
|
|
||||||
if (score >= 60) return AppColors.scoreAverage;
|
|
||||||
return AppColors.scorePoor;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getPositionColor(int position) {
|
|
||||||
if (position == 1) return const Color(0xFFFFD700); // Gold
|
|
||||||
if (position == 2) return const Color(0xFFC0C0C0); // Silver
|
|
||||||
if (position == 3) return const Color(0xFFCD7F32); // Bronze
|
|
||||||
return AppColors.softGrayText;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,322 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
|
||||||
import 'package:quiz_app/data/models/history/participant_history_result.dart';
|
|
||||||
import 'package:quiz_app/feature/admin_result_page/controller/detail_participant_result_controller.dart';
|
|
||||||
|
|
||||||
class ParticipantDetailPage extends GetView<ParticipantResultController> {
|
|
||||||
const ParticipantDetailPage({
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return PopScope(
|
|
||||||
canPop: false,
|
|
||||||
onPopInvokedWithResult: controller.onPop,
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
body: Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
final participant = controller.participantResult.value;
|
|
||||||
if (participant == null) {
|
|
||||||
return const Center(child: Text('Data peserta tidak tersedia.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return SafeArea(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
color: Colors.white,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(LucideIcons.arrowLeft),
|
|
||||||
color: AppColors.darkText,
|
|
||||||
onPressed: controller.goBackPage,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text(
|
|
||||||
'Detail Peserta',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.darkText,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Body Content
|
|
||||||
_buildParticipantHeader(participant),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: participant.answers.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return _buildAnswerCard(participant.answers[index], index + 1);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildParticipantHeader(ParticipantResult participant) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: AppColors.borderLight,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 26,
|
|
||||||
backgroundColor: AppColors.accentBlue,
|
|
||||||
child: Text(
|
|
||||||
controller.participantName.value[0].toUpperCase(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.primaryBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
controller.participantName.value,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
"Jumlah Soal: ${participant.totalQuestions}",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getScoreColor(participant.scorePercent).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: _getScoreColor(participant.scorePercent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
LucideIcons.percent,
|
|
||||||
size: 16,
|
|
||||||
color: _getScoreColor(participant.scorePercent),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
"${participant.scorePercent.toInt()}%",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _getScoreColor(participant.scorePercent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnswerCard(QuestionAnswer answer, int number) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
margin: const EdgeInsets.only(bottom: 20),
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 6,
|
|
||||||
offset: const Offset(0, 3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: answer.isCorrect ? AppColors.primaryBlue.withOpacity(0.1) : AppColors.scorePoor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
number.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: answer.isCorrect ? AppColors.primaryBlue : AppColors.scorePoor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Soal $number: ${answer.question}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
answer.isCorrect ? LucideIcons.checkCircle : LucideIcons.xCircle,
|
|
||||||
color: answer.isCorrect ? AppColors.primaryBlue : AppColors.scorePoor,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Divider(color: AppColors.borderLight),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildAnswerRow(
|
|
||||||
label: "Tipe Soal:",
|
|
||||||
answer: answer.type,
|
|
||||||
isCorrect: true,
|
|
||||||
),
|
|
||||||
_buildAnswerRow(
|
|
||||||
label: "Waktu Diberikan:",
|
|
||||||
answer: "${answer.duration} detik",
|
|
||||||
isCorrect: true,
|
|
||||||
),
|
|
||||||
_buildAnswerRow(
|
|
||||||
label: "Waktu Dihabiskan:",
|
|
||||||
answer: "${answer.timeSpent} detik",
|
|
||||||
isCorrect: true,
|
|
||||||
),
|
|
||||||
_buildAnswerRow(
|
|
||||||
label: "Jawaban Siswa:",
|
|
||||||
answer: answer.answer,
|
|
||||||
isCorrect: answer.isCorrect,
|
|
||||||
),
|
|
||||||
if (!answer.isCorrect) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildAnswerRow(
|
|
||||||
label: "Jawaban Benar:",
|
|
||||||
answer: answer.targetAnswer.toString(),
|
|
||||||
isCorrect: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (answer.options != null) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildOptions(answer.options!),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOptions(List<String> options) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Pilihan Jawaban:",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
...options.map((opt) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Text(
|
|
||||||
"- $opt",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnswerRow({
|
|
||||||
required String label,
|
|
||||||
required String answer,
|
|
||||||
required bool isCorrect,
|
|
||||||
}) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 110,
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
answer,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: isCorrect ? AppColors.primaryBlue : AppColors.scorePoor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getScoreColor(double score) {
|
|
||||||
if (score >= 70) return AppColors.scoreGood;
|
|
||||||
if (score >= 60) return AppColors.scoreAverage;
|
|
||||||
return AppColors.scorePoor;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/data/services/connection_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/quiz_service.dart';
|
|
||||||
import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart';
|
|
||||||
|
|
||||||
class DetailQuizBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
if (!Get.isRegistered<QuizService>()) {
|
|
||||||
Get.lazyPut<QuizService>(() => QuizService());
|
|
||||||
}
|
|
||||||
Get.lazyPut<DetailQuizController>(
|
|
||||||
() => DetailQuizController(
|
|
||||||
Get.find<QuizService>(),
|
|
||||||
Get.find<ConnectionService>(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/routes/app_pages.dart';
|
|
||||||
import 'package:quiz_app/core/helper/connection_check.dart';
|
|
||||||
import 'package:quiz_app/data/models/base/base_model.dart';
|
|
||||||
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
|
|
||||||
import 'package:quiz_app/data/services/connection_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/quiz_service.dart';
|
|
||||||
|
|
||||||
class DetailQuizController extends GetxController {
|
|
||||||
final QuizService _quizService;
|
|
||||||
final ConnectionService _connectionService;
|
|
||||||
|
|
||||||
DetailQuizController(this._quizService, this._connectionService);
|
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
|
||||||
|
|
||||||
QuizData? data;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadData() async {
|
|
||||||
final quizId = Get.arguments as String;
|
|
||||||
if (!await _connectionService.isHaveConnection()) {
|
|
||||||
ConnectionNotification.noInternedConnection();
|
|
||||||
isLoading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getQuizData(quizId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void getQuizData(String quizId) async {
|
|
||||||
BaseResponseModel? response = await _quizService.getQuizById(quizId);
|
|
||||||
if (response != null) {
|
|
||||||
data = response.data;
|
|
||||||
}
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void goToPlayPage() {
|
|
||||||
if (!_connectionService.isCurrentlyConnected) {
|
|
||||||
ConnectionNotification.noInternedConnection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Get.toNamed(AppRoutes.playQuizPage, arguments: data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,178 +0,0 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
|
||||||
import 'package:quiz_app/component/global_button.dart';
|
|
||||||
import 'package:quiz_app/component/widget/loading_widget.dart';
|
|
||||||
import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
|
|
||||||
import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart';
|
|
||||||
|
|
||||||
class DetailQuizView extends GetView<DetailQuizController> {
|
|
||||||
const DetailQuizView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
elevation: 0,
|
|
||||||
title: Text(
|
|
||||||
tr('quiz_detail_title'),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.darkText,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
iconTheme: const IconThemeData(color: AppColors.darkText),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return const Center(child: LoadingWidget());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.data == null) {
|
|
||||||
return const Center(child: Text("Tidak Ditemukan"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Header Section
|
|
||||||
Text(
|
|
||||||
controller.data!.title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
controller.data!.description ?? "",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.calendar_today_rounded, size: 16, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
controller.data!.date ?? "",
|
|
||||||
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Icon(Icons.timer_rounded, size: 16, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
'${controller.data!.limitDuration ~/ 60} ${tr('minutes_suffix')}',
|
|
||||||
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
GlobalButton(text: tr('start_quiz'), onPressed: controller.goToPlayPage),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
const Divider(thickness: 1.2, color: AppColors.borderLight),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Soal Section
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: controller.data!.questionListings.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final question = controller.data!.questionListings[index];
|
|
||||||
return _buildQuestionItem(question, index + 1);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQuestionItem(BaseQuestionModel question, int index) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: AppColors.borderLight),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 6,
|
|
||||||
offset: const Offset(2, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'${tr('question_label')} $index',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
_mapQuestionTypeToText(question.type),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
question.question,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'${tr('duration_label')}: ${question.duration} ${tr('seconds_suffix')}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _mapQuestionTypeToText(String? type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'option':
|
|
||||||
return tr('question_type_option');
|
|
||||||
case 'fill_the_blank':
|
|
||||||
return tr('question_type_fill');
|
|
||||||
case 'true_false':
|
|
||||||
return tr('question_type_true_false');
|
|
||||||
default:
|
|
||||||
return tr('question_type_unknown');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/data/services/history_service.dart';
|
|
||||||
import 'package:quiz_app/feature/history/controller/detail_history_controller.dart';
|
|
||||||
|
|
||||||
class DetailHistoryBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
if (!Get.isRegistered<HistoryService>()) Get.lazyPut(() => HistoryService());
|
|
||||||
Get.lazyPut<DetailHistoryController>(() => DetailHistoryController(Get.find<HistoryService>()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/data/controllers/user_controller.dart';
|
|
||||||
import 'package:quiz_app/data/services/connection_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/history_service.dart';
|
|
||||||
import 'package:quiz_app/feature/history/controller/history_controller.dart';
|
|
||||||
|
|
||||||
class HistoryBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<HistoryService>(() => HistoryService());
|
|
||||||
Get.lazyPut(
|
|
||||||
() => HistoryController(
|
|
||||||
Get.find<HistoryService>(),
|
|
||||||
Get.find<UserController>(),
|
|
||||||
Get.find<ConnectionService>(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import 'package:get/get.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/services/history_service.dart';
|
|
||||||
|
|
||||||
class DetailHistoryController extends GetxController {
|
|
||||||
final HistoryService _historyService;
|
|
||||||
|
|
||||||
DetailHistoryController(this._historyService);
|
|
||||||
|
|
||||||
late QuizAnswerResult quizAnswer;
|
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
_loadData();
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadData() async {
|
|
||||||
String answerId = Get.arguments as String;
|
|
||||||
BaseResponseModel<QuizAnswerResult>? result = await _historyService.getDetailHistory(answerId);
|
|
||||||
if (result != null) {
|
|
||||||
if (result.data != null) quizAnswer = result.data!;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/routes/app_pages.dart';
|
|
||||||
import 'package:quiz_app/core/helper/connection_check.dart';
|
|
||||||
import 'package:quiz_app/data/controllers/user_controller.dart';
|
|
||||||
import 'package:quiz_app/data/models/history/quiz_history.dart';
|
|
||||||
import 'package:quiz_app/data/services/connection_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/history_service.dart';
|
|
||||||
|
|
||||||
class HistoryController extends GetxController {
|
|
||||||
final HistoryService _historyService;
|
|
||||||
final UserController _userController;
|
|
||||||
final ConnectionService _connectionService;
|
|
||||||
|
|
||||||
HistoryController(
|
|
||||||
this._historyService,
|
|
||||||
this._userController,
|
|
||||||
this._connectionService,
|
|
||||||
);
|
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
|
||||||
|
|
||||||
final historyList = <QuizHistory>[].obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
loadHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadHistory() async {
|
|
||||||
if (!await _connectionService.isHaveConnection()) {
|
|
||||||
ConnectionNotification.noInternedConnection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
historyList.value = await _historyService.getHistory(_userController.userData!.id) ?? [];
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void goToDetailHistory(String answerId) => Get.toNamed(AppRoutes.detailHistoryPage, arguments: answerId);
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
// 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';
|
|
||||||
// import 'package:quiz_app/data/models/history/detail_quiz_history.dart';
|
|
||||||
|
|
||||||
// class QuizItemComponent extends StatelessWidget {
|
|
||||||
// final QuestionAnswerItem item;
|
|
||||||
|
|
||||||
// const QuizItemComponent({super.key, required this.item});
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// final bool isOptionType = item.type == 'option';
|
|
||||||
|
|
||||||
// return Container(
|
|
||||||
// width: double.infinity,
|
|
||||||
// margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
||||||
// padding: const EdgeInsets.all(20),
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// color: Colors.white,
|
|
||||||
// borderRadius: BorderRadius.circular(16),
|
|
||||||
// boxShadow: [
|
|
||||||
// BoxShadow(
|
|
||||||
// color: Colors.black.withValues(alpha: 0.04),
|
|
||||||
// blurRadius: 6,
|
|
||||||
// offset: const Offset(0, 2),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// _buildQuestionText(),
|
|
||||||
// const SizedBox(height: 16),
|
|
||||||
// if (isOptionType && item.options != null) _buildOptions(),
|
|
||||||
// const SizedBox(height: 12),
|
|
||||||
// _buildAnswerIndicator(),
|
|
||||||
// const SizedBox(height: 16),
|
|
||||||
// const Divider(height: 24, color: AppColors.shadowPrimary),
|
|
||||||
// _buildMetadata(),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildQuestionText() {
|
|
||||||
// return Text(
|
|
||||||
// '${item.index}. ${item.question}',
|
|
||||||
// style: AppTextStyles.title.copyWith(fontSize: 16, fontWeight: FontWeight.w600),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildOptions() {
|
|
||||||
// return Column(
|
|
||||||
// children: item.options!.asMap().entries.map((entry) {
|
|
||||||
// final int index = entry.key;
|
|
||||||
// final String text = entry.value;
|
|
||||||
|
|
||||||
// final bool isCorrectAnswer = index == item.targetAnswer;
|
|
||||||
// final bool isUserWrongAnswer = index == item.userAnswer && !isCorrectAnswer;
|
|
||||||
|
|
||||||
// Color? backgroundColor;
|
|
||||||
// IconData icon = LucideIcons.circle;
|
|
||||||
// Color iconColor = AppColors.shadowPrimary;
|
|
||||||
|
|
||||||
// if (isCorrectAnswer) {
|
|
||||||
// backgroundColor = AppColors.primaryBlue.withValues(alpha: 0.15);
|
|
||||||
// icon = LucideIcons.checkCircle2;
|
|
||||||
// iconColor = AppColors.primaryBlue;
|
|
||||||
// } else if (isUserWrongAnswer) {
|
|
||||||
// backgroundColor = Colors.red.withValues(alpha: 0.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: backgroundColor,
|
|
||||||
// borderRadius: BorderRadius.circular(12),
|
|
||||||
// border: Border.all(color: AppColors.shadowPrimary),
|
|
||||||
// ),
|
|
||||||
// child: Row(
|
|
||||||
// children: [
|
|
||||||
// Icon(icon, size: 16, color: iconColor),
|
|
||||||
// const SizedBox(width: 8),
|
|
||||||
// Flexible(
|
|
||||||
// child: Text(text, style: AppTextStyles.optionText),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }).toList(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildAnswerIndicator() {
|
|
||||||
// final correctIcon = item.isCorrect ? LucideIcons.checkCircle2 : LucideIcons.xCircle;
|
|
||||||
// final correctColor = item.isCorrect ? AppColors.primaryBlue : Colors.red;
|
|
||||||
|
|
||||||
// final String userAnswerText = item.type == 'option' ? item.options![item.userAnswer] : item.userAnswer.toString();
|
|
||||||
|
|
||||||
// final String correctAnswerText = item.targetAnswer.toString();
|
|
||||||
|
|
||||||
// return Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Row(
|
|
||||||
// children: [
|
|
||||||
// Icon(correctIcon, color: correctColor, size: 18),
|
|
||||||
// const SizedBox(width: 8),
|
|
||||||
// Text(
|
|
||||||
// 'Jawabanmu: $userAnswerText',
|
|
||||||
// style: AppTextStyles.statValue,
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// if (item.type != 'option' && !item.isCorrect) ...[
|
|
||||||
// const SizedBox(height: 6),
|
|
||||||
// Row(
|
|
||||||
// children: [
|
|
||||||
// const SizedBox(width: 26), // offset for icon + spacing
|
|
||||||
// Text(
|
|
||||||
// 'Jawaban benar: $correctAnswerText',
|
|
||||||
// style: AppTextStyles.caption,
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildMetadata() {
|
|
||||||
// return Row(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
// children: [
|
|
||||||
// _metaItem(icon: LucideIcons.helpCircle, label: item.type),
|
|
||||||
// _metaItem(icon: LucideIcons.clock3, label: '${item.timeSpent}s'),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _metaItem({required IconData icon, required String label}) {
|
|
||||||
// return Row(
|
|
||||||
// children: [
|
|
||||||
// Icon(icon, size: 16, color: AppColors.primaryBlue),
|
|
||||||
// const SizedBox(width: 6),
|
|
||||||
// Text(label, style: AppTextStyles.caption),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,150 +0,0 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get_state_manager/get_state_manager.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';
|
|
||||||
import 'package:quiz_app/component/widget/loading_widget.dart';
|
|
||||||
import 'package:quiz_app/component/widget/quiz_item_wa_component.dart';
|
|
||||||
import 'package:quiz_app/feature/history/controller/detail_history_controller.dart';
|
|
||||||
|
|
||||||
class DetailHistoryView extends GetView<DetailHistoryController> {
|
|
||||||
const DetailHistoryView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
elevation: 0,
|
|
||||||
title: Text(
|
|
||||||
context.tr('history_detail_title'),
|
|
||||||
style: AppTextStyles.title.copyWith(fontSize: 24),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
iconTheme: const IconThemeData(color: AppColors.darkText),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return const Center(child: LoadingWidget());
|
|
||||||
}
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
quizMetaInfo(context),
|
|
||||||
...quizListings(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> quizListings() {
|
|
||||||
return controller.quizAnswer.questionListings
|
|
||||||
.asMap()
|
|
||||||
.entries
|
|
||||||
.map((entry) => QuizItemWAComponent(
|
|
||||||
index: entry.key + 1,
|
|
||||||
isCorrect: entry.value.isCorrect,
|
|
||||||
question: entry.value.question,
|
|
||||||
targetAnswer: entry.value.targetAnswer,
|
|
||||||
timeSpent: entry.value.timeSpent,
|
|
||||||
type: entry.value.type,
|
|
||||||
userAnswer: entry.value.userAnswer,
|
|
||||||
options: entry.value.options,
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget quizMetaInfo(BuildContext context) {
|
|
||||||
final quiz = controller.quizAnswer;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(quiz.title, style: AppTextStyles.title),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
quiz.description,
|
|
||||||
textAlign: TextAlign.justify,
|
|
||||||
style: AppTextStyles.caption,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(LucideIcons.calendar, size: 16, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(quiz.answeredAt, style: AppTextStyles.dateTime),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(LucideIcons.clock, size: 16, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text('12:00', style: AppTextStyles.dateTime), // Replace with quiz.timeAnswered if available
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
const Divider(height: 24, thickness: 1, color: AppColors.shadowPrimary),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
_buildStatItem(
|
|
||||||
icon: LucideIcons.checkCircle2,
|
|
||||||
label: tr('correct'),
|
|
||||||
value: "${quiz.totalCorrect}/${quiz.questionListings.length}",
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
_buildStatItem(
|
|
||||||
icon: LucideIcons.award,
|
|
||||||
label: context.tr('score'),
|
|
||||||
value: quiz.totalScore.toString(),
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
),
|
|
||||||
_buildStatItem(
|
|
||||||
icon: LucideIcons.clock3,
|
|
||||||
label: context.tr('time_taken'),
|
|
||||||
value: tr('duration_seconds', namedArgs: {"second": quiz.totalSolveTime.toString()}),
|
|
||||||
color: Colors.orange,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatItem({
|
|
||||||
required IconData icon,
|
|
||||||
required String label,
|
|
||||||
required String value,
|
|
||||||
required Color color,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: color, size: 28),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(value, style: AppTextStyles.statValue),
|
|
||||||
Text(label, style: AppTextStyles.caption),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
|
||||||
import 'package:quiz_app/app/const/text/text_style.dart';
|
|
||||||
import 'package:quiz_app/component/widget/container_skeleton_widget.dart';
|
|
||||||
import 'package:quiz_app/data/models/history/quiz_history.dart';
|
|
||||||
import 'package:quiz_app/feature/history/controller/history_controller.dart';
|
|
||||||
|
|
||||||
class HistoryView extends GetView<HistoryController> {
|
|
||||||
const HistoryView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
body: SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.tr("history_title"),
|
|
||||||
style: AppTextStyles.title.copyWith(fontSize: 24),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
context.tr("history_subtitle"),
|
|
||||||
style: AppTextStyles.subtitle,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: 3,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return ContainerSkeleton();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final historyList = controller.historyList;
|
|
||||||
|
|
||||||
if (historyList.isEmpty) {
|
|
||||||
return Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text(context.tr("no_history"), style: AppTextStyles.body),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: historyList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = historyList[index];
|
|
||||||
return _buildHistoryCard(item);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHistoryCard(QuizHistory item) {
|
|
||||||
final scorePercentage = item.totalCorrect / item.totalQuestion;
|
|
||||||
final scoreColor = scorePercentage >= 0.7 ? AppColors.primaryBlue : AppColors.scorePoor;
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => controller.goToDetailHistory(item.answerId),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.background,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: AppColors.borderLight),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.03),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildIconBox(scoreColor),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: _buildHistoryInfo(item, scorePercentage)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIconBox(Color scoreColor) {
|
|
||||||
return Container(
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: scoreColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.assignment_turned_in,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHistoryInfo(QuizHistory item, double scorePercentage) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
item.title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.darkText,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Completed on ${item.date}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColors.softGrayText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.check_circle_outline, size: 14, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${item.totalCorrect}/${item.totalQuestion} Correct',
|
|
||||||
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Icon(Icons.access_time, size: 14, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
tr("duration_minutes", namedArgs: {"minute": "3"}),
|
|
||||||
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Icon(Icons.percent, size: 14, color: AppColors.softGrayText),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${(scorePercentage * 100).toInt()}%',
|
|
||||||
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/data/controllers/user_controller.dart';
|
|
||||||
import 'package:quiz_app/data/services/connection_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/quiz_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/subject_service.dart';
|
|
||||||
import 'package:quiz_app/feature/home/controller/home_controller.dart';
|
|
||||||
|
|
||||||
class HomeBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<QuizService>(() => QuizService());
|
|
||||||
Get.lazyPut<SubjectService>(() => SubjectService());
|
|
||||||
Get.lazyPut<HomeController>(
|
|
||||||
() => HomeController(
|
|
||||||
Get.find<UserController>(),
|
|
||||||
Get.find<QuizService>(),
|
|
||||||
Get.find<SubjectService>(),
|
|
||||||
Get.find<ConnectionService>(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:quiz_app/app/const/enums/listing_type.dart';
|
|
||||||
import 'package:quiz_app/app/routes/app_pages.dart';
|
|
||||||
import 'package:quiz_app/core/helper/connection_check.dart';
|
|
||||||
import 'package:quiz_app/core/utils/logger.dart';
|
|
||||||
import 'package:quiz_app/data/controllers/user_controller.dart';
|
|
||||||
import 'package:quiz_app/data/models/base/base_model.dart';
|
|
||||||
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
|
|
||||||
import 'package:quiz_app/data/models/subject/subject_model.dart';
|
|
||||||
import 'package:quiz_app/data/services/connection_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/quiz_service.dart';
|
|
||||||
import 'package:quiz_app/data/services/subject_service.dart';
|
|
||||||
import 'package:quiz_app/feature/navigation/controllers/navigation_controller.dart';
|
|
||||||
|
|
||||||
class HomeController extends GetxController {
|
|
||||||
final UserController _userController;
|
|
||||||
final QuizService _quizService;
|
|
||||||
final SubjectService _subjectService;
|
|
||||||
final ConnectionService _connectionService;
|
|
||||||
|
|
||||||
HomeController(
|
|
||||||
this._userController,
|
|
||||||
this._quizService,
|
|
||||||
this._subjectService,
|
|
||||||
this._connectionService,
|
|
||||||
);
|
|
||||||
|
|
||||||
RxInt timeStatus = 1.obs;
|
|
||||||
|
|
||||||
Rx<String> get userName => _userController.userName;
|
|
||||||
Rx<String?> get userImage => _userController.userImage;
|
|
||||||
|
|
||||||
RxList<QuizListingModel> data = <QuizListingModel>[].obs;
|
|
||||||
|
|
||||||
RxList<SubjectModel> subjects = <SubjectModel>[].obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
_getRecomendationQuiz();
|
|
||||||
_getGreetingStatusByTime();
|
|
||||||
loadSubjectData();
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _getRecomendationQuiz() async {
|
|
||||||
if (!await _connectionService.isHaveConnection()) {
|
|
||||||
ConnectionNotification.noInternedConnection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BaseResponseModel? response = await _quizService.recommendationQuiz(userId: _userController.userData!.id);
|
|
||||||
if (response != null) {
|
|
||||||
data.assignAll(response.data as List<QuizListingModel>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadSubjectData() async {
|
|
||||||
if (!_connectionService.isCurrentlyConnected) return;
|
|
||||||
try {
|
|
||||||
final response = await _subjectService.getSubject();
|
|
||||||
subjects.assignAll(response.data!);
|
|
||||||
} catch (e) {
|
|
||||||
logC.e(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void goToQuizCreation() => Get.toNamed(AppRoutes.quizCreatePage);
|
|
||||||
|
|
||||||
void goToRoomMaker() => Get.toNamed(AppRoutes.roomPage);
|
|
||||||
|
|
||||||
void goToJoinRoom() => Get.toNamed(AppRoutes.joinRoomPage);
|
|
||||||
|
|
||||||
void goToSearch() {
|
|
||||||
final navController = Get.find<NavigationController>();
|
|
||||||
navController.changePage(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onRecommendationTap(String quizId) => Get.toNamed(AppRoutes.detailQuizPage, arguments: quizId);
|
|
||||||
|
|
||||||
void goToListingsQuizPage(ListingType page, {String? subjectId, String? subjecName}) => Get.toNamed(
|
|
||||||
AppRoutes.listingQuizPage,
|
|
||||||
arguments: {"page": page, "id": subjectId, "subject_name": subjecName},
|
|
||||||
);
|
|
||||||
|
|
||||||
void _getGreetingStatusByTime() {
|
|
||||||
final hour = DateTime.now().hour;
|
|
||||||
|
|
||||||
if (hour >= 5 && hour < 12) {
|
|
||||||
timeStatus.value = 1;
|
|
||||||
} else if (hour >= 12 && hour < 15) {
|
|
||||||
timeStatus.value = 2;
|
|
||||||
} else if (hour >= 15 && hour < 18) {
|
|
||||||
timeStatus.value = 3;
|
|
||||||
} else {
|
|
||||||
timeStatus.value = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue