feat: adjustment on the interface play quiz multiplayer

This commit is contained in:
akhdanre 2025-05-23 19:30:46 +07:00
parent c2d838d17d
commit 20017d5bf1
1 changed files with 561 additions and 178 deletions

View File

@ -17,7 +17,7 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
} }
if (controller.currentQuestion.value == null) { if (controller.currentQuestion.value == null) {
return const Center(child: CircularProgressIndicator()); return _buildLoadingView();
} }
return _buildQuestionView(); return _buildQuestionView();
@ -26,133 +26,340 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
); );
} }
Widget _buildQuestionView() { Widget _buildLoadingView() {
final question = controller.currentQuestion.value!; return Center(
return SafeArea(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Custom AppBar content moved to body TweenAnimationBuilder<double>(
Padding( tween: Tween(begin: 0.0, end: 1.0),
padding: const EdgeInsets.all(16.0), duration: const Duration(milliseconds: 1500),
child: Text( builder: (context, value, child) {
"Soal ${(question.questionIndex)}/10", return Transform.rotate(
style: const TextStyle( angle: value * 6.28,
color: Colors.black, child: Container(
fontWeight: FontWeight.bold, width: 60,
fontSize: 24, height: 60,
), decoration: BoxDecoration(
), shape: BoxShape.circle,
), border: Border.all(
color: const Color(0xFF2563EB),
Obx(() { width: 4,
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Time remaining text
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Waktu tersisa:",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
Text(
"${controller.remainingTime.value} detik",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB),
),
),
],
),
const SizedBox(height: 8),
// Progress bar
LinearProgressIndicator(
value: controller.remainingTime.value / question.duration,
minHeight: 8,
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(
controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB),
), ),
borderRadius: BorderRadius.circular(4),
), ),
], child: const Center(
), child: Icon(
); Icons.quiz,
}), color: Color(0xFF2563EB),
size: 30,
const SizedBox(height: 20), ),
Obx(() {
if (controller.isASentAns.value) {
return Container(
padding: const EdgeInsets.all(20),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Add a nice loading animation
const SizedBox(height: 24),
// Improved text with better styling
const Text(
"Jawaban Anda telah terkirim",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 8),
// Informative subtext
const Text(
"Mohon tunggu soal selanjutnya",
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
), ),
), ),
); );
} },
),
const SizedBox(height: 20),
const Text(
"Memuat soal...",
style: TextStyle(
fontSize: 16,
color: Colors.black54,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
return Expanded( Widget _buildQuestionView() {
child: Padding( final question = controller.currentQuestion.value!;
padding: const EdgeInsets.all(20.0), return SafeArea(
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(16),
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
question.question, children: [
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black), _buildCustomAppBar(question),
), const SizedBox(height: 20),
const SizedBox(height: 20), _buildProgressSection(question),
if (question.type == 'option') _buildOptionQuestion(), const SizedBox(height: 20),
if (question.type == 'fill_the_blank') _buildFillInBlankQuestion(), _buildQuestionCard(question),
if (question.type == 'true_false') _buildTrueFalseQuestion(), const SizedBox(height: 30),
const Spacer(), Expanded(child: _buildAnswerSection()),
Obx( _buildSubmitButton(),
() => GlobalButton( ],
text: "Kirim jawaban", ),
onPressed: controller.submitAnswer, ),
type: controller.buttonType.value, );
), }
)
], Widget _buildCustomAppBar(dynamic question) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF2563EB).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
"Soal ${question.questionIndex}/10",
style: const TextStyle(
color: Color(0xFF2563EB),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Row(
children: [
Icon(
Icons.people,
size: 20,
color: const Color(0xFF2563EB),
),
const SizedBox(width: 4),
Text(
"Multiplayer",
style: TextStyle(
color: const Color(0xFF2563EB),
fontWeight: FontWeight.w600,
fontSize: 14,
), ),
), ),
); ],
}), ),
// Question content Obx(() => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: controller.remainingTime.value <= 10 ? Colors.red.withOpacity(0.1) : const Color(0xFF2563EB).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(
Icons.timer_outlined,
size: 16,
color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB),
),
const SizedBox(width: 4),
Text(
"${controller.remainingTime.value}s",
style: TextStyle(
color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
)),
],
),
);
}
Widget _buildProgressSection(dynamic question) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Progress",
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
Text(
"${(question.questionIndex * 10).toInt()}%",
style: const TextStyle(
fontSize: 14,
color: Color(0xFF2563EB),
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.grey[300],
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: question.questionIndex / 10,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
gradient: const LinearGradient(
colors: [Color(0xFF2563EB), Color(0xFF1E40AF)],
),
),
),
),
),
const SizedBox(height: 12),
Obx(() => AnimatedContainer(
duration: const Duration(milliseconds: 100),
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: Colors.grey[300],
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: controller.remainingTime.value / question.duration,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: controller.remainingTime.value > question.duration * 0.3
? Colors.green
: controller.remainingTime.value > question.duration * 0.1
? Colors.orange
: Colors.red,
),
),
),
)),
],
);
}
Widget _buildQuestionCard(dynamic question) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.3, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
child: Container(
key: ValueKey(question.question),
padding: const EdgeInsets.all(20),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Text(
question.question,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
height: 1.4,
),
),
),
);
}
Widget _buildAnswerSection() {
return Obx(() {
if (controller.isASentAns.value) {
return _buildWaitingView();
}
final question = controller.currentQuestion.value!;
if (question.type == 'option') return _buildOptionQuestion();
if (question.type == 'fill_the_blank') return _buildFillInBlankQuestion();
if (question.type == 'true_false') return _buildTrueFalseQuestion();
return const SizedBox();
});
}
Widget _buildWaitingView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 2000),
builder: (context, value, child) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green.withOpacity(0.1),
border: Border.all(
color: Colors.green,
width: 3,
),
),
child: Transform.scale(
scale: 0.8 + (value * 0.2),
child: const Icon(
Icons.check_circle,
size: 40,
color: Colors.green,
),
),
);
},
),
const SizedBox(height: 24),
const Text(
"Jawaban Terkirim!",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
const SizedBox(height: 8),
Text(
"Menunggu soal selanjutnya...",
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 20),
Container(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey[400]!),
),
),
], ],
), ),
); );
@ -162,65 +369,204 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
final options = controller.currentQuestion.value!.options; final options = controller.currentQuestion.value!.options;
return Column( return Column(
children: List.generate(options!.length, (index) { children: List.generate(options!.length, (index) {
final option = options[index]; return _buildOptionButton(options[index], index);
final isSelected = controller.selectedAnswer.value == index.toString();
return Container(
margin: const EdgeInsets.only(bottom: 12),
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isSelected ? const Color(0xFF2563EB) : Colors.white,
foregroundColor: isSelected ? Colors.white : Colors.black,
side: const BorderSide(color: Colors.grey),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => controller.selectOptionAnswer(index),
child: Text(option),
),
);
}), }),
); );
} }
Widget _buildOptionButton(String option, int index) {
return Obx(() {
final isSelected = controller.selectedAnswer.value == index.toString();
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
width: double.infinity,
child: Material(
elevation: isSelected ? 8 : 2,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => controller.selectOptionAnswer(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2563EB) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFF2563EB) : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? Colors.white : Colors.transparent,
border: Border.all(
color: isSelected ? Colors.white : Colors.grey,
width: 2,
),
),
child: isSelected ? const Icon(Icons.check, size: 16, color: Color(0xFF2563EB)) : null,
),
const SizedBox(width: 16),
Expanded(
child: Text(
option,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isSelected ? Colors.white : Colors.black87,
),
),
),
],
),
),
),
),
);
});
}
Widget _buildFillInBlankQuestion() { Widget _buildFillInBlankQuestion() {
return Column( return Container(
children: [ padding: const EdgeInsets.all(20),
GlobalTextField(controller: controller.fillInAnswerController), decoration: BoxDecoration(
const SizedBox(height: 20), color: Colors.white,
], borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Masukkan jawaban Anda:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
const SizedBox(height: 16),
GlobalTextField(controller: controller.fillInAnswerController),
],
),
); );
} }
Widget _buildTrueFalseQuestion() { Widget _buildTrueFalseQuestion() {
return Column( return Column(
children: [ children: [
_buildTrueFalseButton('Ya', true), Row(
_buildTrueFalseButton('Tidak', false), children: [
Expanded(
child: _buildTrueFalseButton(
'Ya',
true,
Icons.check_circle,
Colors.green,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildTrueFalseButton(
'Tidak',
false,
Icons.cancel,
Colors.red,
),
),
],
),
], ],
); );
} }
Widget _buildTrueFalseButton(String label, bool value) { Widget _buildTrueFalseButton(String label, bool value, IconData icon, Color color) {
final isSelected = controller.selectedAnswer.value == value.toString(); return Obx(() {
final isSelected = controller.selectedAnswer.value == value.toString();
return Container( return AnimatedContainer(
margin: const EdgeInsets.only(bottom: 12), duration: const Duration(milliseconds: 200),
width: double.infinity, height: 120,
child: ElevatedButton.icon( child: Material(
style: ElevatedButton.styleFrom( elevation: isSelected ? 8 : 2,
backgroundColor: isSelected ? (value ? Colors.green : Colors.red) : Colors.white, borderRadius: BorderRadius.circular(20),
foregroundColor: isSelected ? Colors.white : Colors.black, child: InkWell(
side: const BorderSide(color: Colors.grey), borderRadius: BorderRadius.circular(20),
padding: const EdgeInsets.symmetric(vertical: 14), onTap: () => controller.selectTrueFalseAnswer(value),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Container(
decoration: BoxDecoration(
color: isSelected ? color.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? color : Colors.grey.shade300,
width: isSelected ? 3 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? color : Colors.grey.shade100,
),
child: Icon(
icon,
size: 32,
color: isSelected ? Colors.white : Colors.grey,
),
),
const SizedBox(height: 12),
Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isSelected ? color : Colors.black87,
),
),
],
),
),
),
), ),
onPressed: () => controller.selectTrueFalseAnswer(value), );
icon: Icon(value ? Icons.check_circle_outline : Icons.cancel_outlined), });
label: Text(label), }
),
); Widget _buildSubmitButton() {
return Obx(() {
if (controller.isASentAns.value) {
return const SizedBox.shrink();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.only(top: 20),
child: GlobalButton(
text: "Kirim Jawaban",
onPressed: controller.submitAnswer,
type: controller.buttonType.value,
),
);
});
} }
Widget _buildDoneView() { Widget _buildDoneView() {
@ -230,32 +576,69 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( TweenAnimationBuilder<double>(
Icons.check_circle, tween: Tween(begin: 0.0, end: 1.0),
size: 80, duration: const Duration(milliseconds: 800),
color: Color(0xFF2563EB), builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF2563EB).withOpacity(0.1),
border: Border.all(
color: const Color(0xFF2563EB),
width: 4,
),
),
child: const Icon(
Icons.emoji_events,
size: 60,
color: Color(0xFF2563EB),
),
),
);
},
), ),
const SizedBox(height: 20), const SizedBox(height: 32),
const Text( const Text(
"Kuis telah selesai!", "Kuis Selesai!",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( Text(
"Terima kasih telah berpartisipasi.", "Terima kasih telah berpartisipasi dalam kuis multiplayer.",
style: TextStyle(fontSize: 16, color: Colors.black54), style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
height: 1.5,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
ElevatedButton( Container(
onPressed: controller.goToDetailResult, width: double.infinity,
style: ElevatedButton.styleFrom( height: 56,
backgroundColor: const Color(0xFF2563EB), child: ElevatedButton.icon(
foregroundColor: Colors.white, onPressed: controller.goToDetailResult,
minimumSize: const Size(double.infinity, 50), style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), backgroundColor: const Color(0xFF2563EB),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 6,
),
icon: const Icon(Icons.assessment),
label: const Text(
"Lihat Hasil",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
), ),
child: const Text("Lihat Hasil", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
), ),
], ],
), ),