diff --git a/lib/core/services/constants.dart b/lib/core/services/constants.dart index eb6e096..5b0d470 100644 --- a/lib/core/services/constants.dart +++ b/lib/core/services/constants.dart @@ -1 +1 @@ -const String baseUrl = 'https://8b78-114-6-25-184.ngrok-free.app/'; +const String baseUrl = 'https://7333-114-6-25-184.ngrok-free.app/'; diff --git a/lib/core/services/dio_client.dart b/lib/core/services/dio_client.dart index c7422bf..121e8ea 100644 --- a/lib/core/services/dio_client.dart +++ b/lib/core/services/dio_client.dart @@ -9,7 +9,7 @@ class DioClient { DioClient() { _dio.options.baseUrl = baseUrl; _dio.options.connectTimeout = const Duration(seconds: 5); - _dio.options.receiveTimeout = const Duration(seconds: 3); + _dio.options.receiveTimeout = const Duration(seconds: 10); } Future refreshAccessToken(String refreshToken) async { diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index dfd1eb3..b9971f5 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -439,8 +439,7 @@ class _HomeContentState extends State { ), completedTopicsProvider.isLoading ? _buildShimmerEffect() - : completedTopicsProvider.completedTopics == null || - completedTopicsProvider.completedTopics.isEmpty + : completedTopicsProvider.completedTopics.isEmpty ? _buildNoDataWidget() : _buildCompletedTopicsContent( completedTopicsProvider), diff --git a/lib/features/home/widgets/progress_card.dart b/lib/features/home/widgets/progress_card.dart index 58eae0d..69aa711 100644 --- a/lib/features/home/widgets/progress_card.dart +++ b/lib/features/home/widgets/progress_card.dart @@ -16,7 +16,7 @@ class ProgressCard extends StatelessWidget { String _getFullImageUrl(String thumbnail) { return thumbnail.startsWith('http') ? thumbnail - : '${baseUrl}uploads/section/$thumbnail'; + : '${baseUrl}api/uploads/section/$thumbnail'; } Future _navigateToTopics( diff --git a/lib/features/learning/modules/exercises/providers/exercise_provider.dart b/lib/features/learning/modules/exercises/providers/exercise_provider.dart index 7819b75..691f2fb 100644 --- a/lib/features/learning/modules/exercises/providers/exercise_provider.dart +++ b/lib/features/learning/modules/exercises/providers/exercise_provider.dart @@ -27,6 +27,8 @@ class ExerciseProvider extends ChangeNotifier { String _nameLevel = ''; String? _activeLeftOption; String? _studentLearningId; + List> _shuffledRightPairsList = []; + List> _savedShuffledRightPairsList = []; // Constants final List _pairColors = [ @@ -47,6 +49,9 @@ class ExerciseProvider extends ChangeNotifier { String? get activeLeftOption => _activeLeftOption; String? get studentLearningId => _studentLearningId; List get reviewExercises => _reviewExercises; + List> get shuffledRightPairsList => _shuffledRightPairsList; + List> get savedShuffledRightPairsList => + _savedShuffledRightPairsList; // Initialization methods void initializeAnswers() { @@ -69,6 +74,35 @@ class ExerciseProvider extends ChangeNotifier { }); } + // Method untuk menyimpan urutan acak saat submit + void saveShuffledRightPairs() { + _savedShuffledRightPairsList = List.from(_shuffledRightPairsList); + } + + // Method untuk mengatur ulang urutan acak saat review + void restoreShuffledRightPairs() { + if (_savedShuffledRightPairsList.isNotEmpty) { + _shuffledRightPairsList = List.from(_savedShuffledRightPairsList); + } + } + + void initializeShuffledRightPairs(List exercises) { + // Hanya acak sekali saat pertama kali membuka level + if (_shuffledRightPairsList.isEmpty) { + _shuffledRightPairsList = exercises.map((exercise) { + if (exercise.choices is MatchingPair) { + final pairs = (exercise.choices as MatchingPair).pairs; + final rightPairs = pairs.map((pair) => pair.right).toList(); + + // Buat salinan dan acak + final shuffledPairs = List.from(rightPairs)..shuffle(); + return shuffledPairs; + } + return []; + }).toList(); + } + } + // // Answer handling methods // void answerQuestion(int index, String answer) { // if (index >= 0 && index < _answers.length) { @@ -109,6 +143,20 @@ class ExerciseProvider extends ChangeNotifier { } } + bool hasMatchedPair(int exerciseIndex, String rightOption) { + if (!_matchingAnswers.containsKey(exerciseIndex)) return false; + return _matchingAnswers[exerciseIndex]! + .any((pair) => pair['right'] == rightOption); + } + + String? getMatchedLeftPair(int exerciseIndex, String rightOption) { + if (!_matchingAnswers.containsKey(exerciseIndex)) return null; + final matchedPair = _matchingAnswers[exerciseIndex]!.firstWhere( + (pair) => pair['right'] == rightOption, + orElse: () => {'left': '', 'right': ''}); + return matchedPair['left']!.isEmpty ? null : matchedPair['left']; + } + void _handleMatchingPairAnswer(int exerciseIndex, String option) { final matchingPair = _exercises[exerciseIndex].choices as MatchingPair; final isLeft = matchingPair.pairs.any((pair) => pair.left == option); @@ -137,27 +185,20 @@ class ExerciseProvider extends ChangeNotifier { return; } - final pairs = (_exercises[exerciseIndex].choices as MatchingPair).pairs; - final leftIndex = - pairs.indexWhere((pair) => pair.left == _activeLeftOption); - final rightIndex = pairs.indexWhere((pair) => pair.right == option); - - if (leftIndex != -1 && rightIndex != -1) { - if (!_matchingAnswers.containsKey(exerciseIndex)) { - _matchingAnswers[exerciseIndex] = []; - } - - _matchingAnswers[exerciseIndex]!.removeWhere((pair) => - pair['left'] == _activeLeftOption || pair['right'] == option); - - _matchingAnswers[exerciseIndex]! - .add({'left': _activeLeftOption!, 'right': option}); - - _rightColors[exerciseIndex][rightIndex] = - _leftColors[exerciseIndex][leftIndex]; - - _activeLeftOption = null; + if (!_matchingAnswers.containsKey(exerciseIndex)) { + _matchingAnswers[exerciseIndex] = []; } + + // Remove any existing matches for this left or right option + _matchingAnswers[exerciseIndex]!.removeWhere( + (pair) => pair['left'] == _activeLeftOption || pair['right'] == option); + + // Add the new pair + _matchingAnswers[exerciseIndex]! + .add({'left': _activeLeftOption!, 'right': option}); + + // Reset active left option + _activeLeftOption = null; } void _updateMatchingPairAnswer(int exerciseIndex) { @@ -260,11 +301,16 @@ class ExerciseProvider extends ChangeNotifier { // API methods Future fetchExercises(String levelId) async { + if (_isLoading) return; // Hindari multiple calls + _isLoading = true; _currentExerciseIndex = 0; _resetReviewState(); notifyListeners(); + // Reset shuffled pairs + _shuffledRightPairsList = []; + try { final token = await _userProvider.getValidToken(); if (token == null) { @@ -275,9 +321,16 @@ class ExerciseProvider extends ChangeNotifier { _nameTopic = data['NAME_TOPIC']; _nameLevel = data['NAME_LEVEL']; final exercisesData = data['EXERCISES']; + + // Map exercises _exercises = exercisesData .map((json) => ExerciseModel.fromJson(json)) .toList(); + + // Inisialisasi pengacakan untuk matching pairs + initializeShuffledRightPairs(_exercises); + + // Inisialisasi jawaban _answers = List.generate(_exercises.length, (index) => ''); initializeAnswers(); } catch (e) { @@ -305,6 +358,8 @@ class ExerciseProvider extends ChangeNotifier { final payload = response['payload']; _reviewData = ReviewExerciseModel.fromJson(payload); + restoreShuffledRightPairs(); + // Sort the exercises based on the title number _reviewExercises = (_reviewData?.stdExercises ?? []) ..sort((a, b) { @@ -376,7 +431,8 @@ class ExerciseProvider extends ChangeNotifier { } Future> submitAnswersAndGetScore() async { - print('submitAnswersAndGetScore called'); + // Simpan urutan acak sebelum submit + saveShuffledRightPairs(); try { if (_studentLearningId == null) { throw Exception('Student Learning ID is not set'); diff --git a/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart b/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart index 2cada5f..d5b4e3c 100644 --- a/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart +++ b/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart @@ -168,7 +168,7 @@ class _ExerciseContentState extends State padding: const EdgeInsets.symmetric(vertical: 16.0), child: ImageWidget( imageFileName: widget.exercise.image!, - baseUrl: '${baseUrl}uploads/exercise/image/', + baseUrl: '${baseUrl}api/uploads/exercise/image/', ), ), ), @@ -180,7 +180,7 @@ class _ExerciseContentState extends State child: AudioPlayerWidget( key: _audioPlayerKey, audioFileName: widget.exercise.audio!, - baseUrl: '${baseUrl}uploads/exercise/audio/', + baseUrl: '${baseUrl}api/uploads/exercise/audio/', ), ), ), diff --git a/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart b/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart index 9070415..a07e9ff 100644 --- a/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart +++ b/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart @@ -6,7 +6,7 @@ import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart'; import 'package:provider/provider.dart'; -class MatchingPairsQuestion extends StatelessWidget { +class MatchingPairsQuestion extends StatefulWidget { final dynamic exercise; final bool isReview; @@ -17,108 +17,155 @@ class MatchingPairsQuestion extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final provider = Provider.of(context); - List pairs = []; - Map studentAnswers = {}; - final currentIndex = provider.currentExerciseIndex; + State createState() => _MatchingPairsQuestionState(); +} - if (exercise is ExerciseModel) { - pairs = (exercise.choices as MatchingPair).pairs; - } else if (exercise is ReviewExerciseDetail) { - if (exercise.matchingPairs != null) { - pairs = exercise.matchingPairs!.map((reviewPair) { +class _MatchingPairsQuestionState extends State { + late List pairs; + late List originalRightPairs; + late List shuffledRightPairs; + late Map studentAnswers; + late Map originalPairMapping; + late Map originalPairIndices; + + @override + void initState() { + super.initState(); + initializePairs(); + } + + // void initializePairs() { + // pairs = []; + // studentAnswers = {}; + // originalPairMapping = {}; + // originalPairIndices = {}; + + // if (widget.exercise is ExerciseModel) { + // // Mode Exercise + // pairs = (widget.exercise.choices as MatchingPair).pairs; + // final provider = Provider.of(context, listen: false); + // final currentIndex = provider.currentExerciseIndex; + + // // Simpan indeks asli untuk setiap pasangan kiri + // for (int i = 0; i < pairs.length; i++) { + // originalPairIndices[pairs[i].left] = i; + // } + + // originalRightPairs = pairs.map((pair) => pair.right as String).toList(); + // shuffledRightPairs = + // List.from(provider.shuffledRightPairsList[currentIndex]); + // } else if (widget.exercise is ReviewExerciseDetail) { + // // Mode Review + // if (widget.exercise.matchingPairs != null) { + // if (widget.exercise.answerStudent.isNotEmpty) { + // final answerPairs = widget.exercise.answerStudent.split(', '); + + // // Buat pairs berdasarkan urutan jawaban siswa + // pairs = answerPairs.map((pair) { + // final parts = pair.split('-'); + // return Pair(left: parts[0], right: parts[1]); + // }).toList(); + + // // Simpan indeks asli dan mapping untuk setiap pasangan + // for (int i = 0; i < pairs.length; i++) { + // final left = pairs[i].left; + // final right = pairs[i].right; + // originalPairIndices[left] = i; + // studentAnswers[left] = right; + // originalPairMapping[left] = right; + // } + + // // Set originalRightPairs dan shuffledRightPairs sama dengan urutan jawaban siswa + // originalRightPairs = + // pairs.map((pair) => pair.right as String).toList(); + // shuffledRightPairs = + // originalRightPairs.toList(); // Tidak diacak untuk review + // } else { + // // Jika tidak ada jawaban, gunakan data asli + // pairs = widget.exercise.matchingPairs!.map((reviewPair) { + // return Pair( + // left: reviewPair.leftPair, + // right: reviewPair.rightPair, + // ); + // }).toList(); + + // // Simpan indeks asli untuk setiap pasangan + // for (int i = 0; i < pairs.length; i++) { + // originalPairIndices[pairs[i].left] = i; + // } + + // originalRightPairs = + // pairs.map((pair) => pair.right as String).toList(); + // shuffledRightPairs = originalRightPairs.toList(); + // } + // } + // } + + // // Bangun pemetaan konsisten + // for (int i = 0; i < pairs.length; i++) { + // final left = pairs[i].left; + // final right = pairs[i].right; + // originalPairMapping[left] = right; + // } + // } + + void initializePairs() { + pairs = []; + studentAnswers = {}; + originalPairMapping = {}; + originalPairIndices = {}; + + if (widget.exercise is ExerciseModel) { + // Mode Exercise + pairs = (widget.exercise.choices as MatchingPair).pairs; + final provider = Provider.of(context, listen: false); + final currentIndex = provider.currentExerciseIndex; + + // Simpan indeks asli untuk setiap pasangan kiri + for (int i = 0; i < pairs.length; i++) { + originalPairIndices[pairs[i].left] = i; + } + + originalRightPairs = pairs.map((pair) => pair.right as String).toList(); + shuffledRightPairs = + List.from(provider.shuffledRightPairsList[currentIndex]); + } else if (widget.exercise is ReviewExerciseDetail) { + // Mode Review + if (widget.exercise.matchingPairs != null) { + // Gunakan data asli dari exercise untuk mempertahankan urutan + pairs = widget.exercise.matchingPairs!.map((reviewPair) { return Pair( left: reviewPair.leftPair, right: reviewPair.rightPair, ); }).toList(); - // Parse student answers - if (exercise.answerStudent.isNotEmpty) { - final answerPairs = exercise.answerStudent.split(', '); - for (var pair in answerPairs) { - final parts = pair.split('-'); - if (parts.length == 2) { - studentAnswers[parts[0]] = parts[1]; - } - } + // Simpan indeks asli untuk setiap pasangan + for (int i = 0; i < pairs.length; i++) { + originalPairIndices[pairs[i].left] = i; + } + + // Use the original right pairs without shuffling + originalRightPairs = pairs.map((pair) => pair.right as String).toList(); + shuffledRightPairs = + originalRightPairs.toList(); // No shuffling for review + + // Rekonstruksi jawaban siswa + if (widget.exercise.answerStudent.isNotEmpty) { + final answerPairs = widget.exercise.answerStudent.split(', '); + studentAnswers = { + for (var pair in answerPairs) pair.split('-')[0]: pair.split('-')[1] + }; } } } - // Create a reverse mapping for easier lookup of left pair from right pair - Map reverseStudentAnswers = {}; - studentAnswers.forEach((left, right) { - reverseStudentAnswers[right] = left; - }); - - return Column( - children: [ - ...pairs.asMap().entries.map((entry) { - int pairIndex = entry.key; - dynamic pair = entry.value; - String left = pair is Pair ? pair.left : pair.leftPair; - String right = pair is Pair ? pair.right : pair.rightPair; - - // Get the corresponding color for this pair - Color pairColor = _getPairColor(pairIndex); - - // Find if this item is matched in student answers - String? studentMatchedRight = studentAnswers[left]; - String? matchedLeftForRight = reverseStudentAnswers[right]; - - // Find the color index for the matched pair - int? matchedPairIndex; - if (isReview && matchedLeftForRight != null) { - matchedPairIndex = pairs.indexWhere((p) { - if (p is Pair) { - return p.left == matchedLeftForRight; - } - return false; - }); - } - - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: _buildOptionItem( - context, - left, - provider, - currentIndex, - pairIndex, - true, - studentMatchedRight != null, - studentAnswers, - pairColor, - ), - ), - const SizedBox(width: 10), - Expanded( - child: _buildOptionItem( - context, - right, - provider, - currentIndex, - matchedPairIndex ?? pairIndex, - false, - matchedLeftForRight != null, - studentAnswers, - matchedPairIndex != null - ? _getPairColor(matchedPairIndex) - : pairColor, - ), - ), - ], - ), - ); - }).toList(), - ], - ); + // Bangun pemetaan konsisten + for (int i = 0; i < pairs.length; i++) { + final left = pairs[i].left; + final right = pairs[i].right; + originalPairMapping[left] = right; + } } Color _getPairColor(int index) { @@ -137,33 +184,84 @@ class MatchingPairsQuestion extends StatelessWidget { String option, ExerciseProvider provider, int exerciseIndex, - int pairIndex, bool isLeft, - bool isMatched, Map studentAnswers, - Color pairColor, ) { - Color? color; bool isSelected = false; bool isActive = false; + Color? color; - if (isReview) { - if (isMatched) { - color = pairColor; - isSelected = true; + // Mencari indeks asli pasangan untuk warna yang konsisten + int originalPairIndex = -1; + + if (isLeft) { + originalPairIndex = pairs.indexWhere((pair) => + pair is Pair ? pair.left == option : pair.leftPair == option); + isActive = provider.activeLeftOption == option; + } else { + // Untuk right pair, cari berdasarkan pasangan yang sudah terbentuk + if (widget.isReview) { + // Untuk mode review, periksa apakah opsi ini ada di jawaban siswa + final matchedLeft = studentAnswers.keys.firstWhere( + (key) => studentAnswers[key] == option, + orElse: () => '', + ); + + if (matchedLeft.isNotEmpty) { + originalPairIndex = pairs.indexWhere((pair) => pair is Pair + ? pair.left == matchedLeft + : pair.leftPair == matchedLeft); + isSelected = true; + } + } else { + if (provider.hasMatchedPair(exerciseIndex, option)) { + String? matchedLeft = + provider.getMatchedLeftPair(exerciseIndex, option); + if (matchedLeft != null) { + originalPairIndex = pairs.indexWhere((pair) => pair is Pair + ? pair.left == matchedLeft + : pair.leftPair == matchedLeft); + } + } + } + } + + // Menentukan warna berdasarkan status + if (widget.isReview) { + if (isLeft) { + // Untuk left pair di mode review + final matchedRight = studentAnswers[option]; + if (matchedRight != null) { + isSelected = true; + // Cari indeks pasangan untuk warna yang konsisten + originalPairIndex = pairs.indexWhere( + (pair) => (pair is Pair ? pair.left : pair.leftPair) == option); + color = _getPairColor(originalPairIndex); + } + } else { + // Untuk right pair di mode review + final matchedLeft = studentAnswers.keys.firstWhere( + (key) => studentAnswers[key] == option, + orElse: () => '', + ); + + if (matchedLeft.isNotEmpty) { + isSelected = true; + // Cari indeks pasangan untuk warna yang konsisten + originalPairIndex = pairs.indexWhere((pair) => + (pair is Pair ? pair.left : pair.leftPair) == matchedLeft); + color = _getPairColor(originalPairIndex); + } } } else { - if (isLeft) { - color = provider.getLeftColor(exerciseIndex, pairIndex); - isActive = provider.activeLeftOption == option; - } else { - color = provider.getRightColor(exerciseIndex, pairIndex); - } isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft); + if (isSelected || isActive) { + color = _getPairColor(originalPairIndex); + } } return GestureDetector( - onTap: isReview + onTap: widget.isReview ? null : () { if (!isLeft && provider.activeLeftOption == null) { @@ -179,8 +277,10 @@ class MatchingPairsQuestion extends StatelessWidget { child: Container( decoration: BoxDecoration( color: isSelected - ? color ?? pairColor - : (isActive ? pairColor : AppColors.whiteColor), + ? color ?? _getPairColor(originalPairIndex) + : (isActive + ? _getPairColor(originalPairIndex) + : AppColors.whiteColor), borderRadius: const BorderRadius.only( topRight: Radius.circular(20), bottomRight: Radius.circular(20), @@ -192,10 +292,10 @@ class MatchingPairsQuestion extends StatelessWidget { : AppColors.cardDisabledColor, width: isSelected ? 2 : 1, ), - boxShadow: isActive && !isReview + boxShadow: isActive && !widget.isReview ? [ BoxShadow( - color: pairColor.withOpacity(0.5), + color: _getPairColor(originalPairIndex).withOpacity(0.5), spreadRadius: 1, blurRadius: 4, offset: const Offset(0, 2), @@ -216,4 +316,52 @@ class MatchingPairsQuestion extends StatelessWidget { ), ); } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + final currentIndex = provider.currentExerciseIndex; + + return Column( + children: [ + ...pairs.asMap().entries.map((entry) { + final int pairIndex = entry.key; + final dynamic pair = entry.value; + final String left = pair is Pair ? pair.left : pair.leftPair; + // Menggunakan shuffledRightPairs untuk right pair + final String right = shuffledRightPairs[pairIndex]; + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildOptionItem( + context, + left, + provider, + currentIndex, + true, + studentAnswers, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildOptionItem( + context, + right, + provider, + currentIndex, + false, + studentAnswers, + ), + ), + ], + ), + ); + }).toList(), + ], + ); + } } diff --git a/lib/features/learning/modules/model/section_model.dart b/lib/features/learning/modules/model/section_model.dart index 229f407..5cde224 100644 --- a/lib/features/learning/modules/model/section_model.dart +++ b/lib/features/learning/modules/model/section_model.dart @@ -2,14 +2,14 @@ class Section { final String id; final String name; final String description; - final String thumbnail; + final String? thumbnail; final DateTime timeSection; Section({ required this.id, required this.name, required this.description, - required this.thumbnail, + this.thumbnail, required this.timeSection, }); diff --git a/lib/features/learning/modules/topics/screens/topics_list_screen.dart b/lib/features/learning/modules/topics/screens/topics_list_screen.dart index f900231..b07e116 100644 --- a/lib/features/learning/modules/topics/screens/topics_list_screen.dart +++ b/lib/features/learning/modules/topics/screens/topics_list_screen.dart @@ -32,7 +32,7 @@ class _TopicsListScreenState extends State { if (thumbnail.startsWith('http')) { return thumbnail; } else { - return '${baseUrl}uploads/section/$thumbnail'; + return '${baseUrl}api/uploads/section/$thumbnail'; } } @@ -84,7 +84,7 @@ class _TopicsListScreenState extends State { Stack( children: [ Image.network( - _getFullImageUrl(selectedSection.thumbnail), + _getFullImageUrl(selectedSection.thumbnail ?? ''), fit: BoxFit.cover, width: double.infinity, height: 115, diff --git a/lib/features/learning/provider/section_provider.dart b/lib/features/learning/provider/section_provider.dart index 722aaa2..895d3f8 100644 --- a/lib/features/learning/provider/section_provider.dart +++ b/lib/features/learning/provider/section_provider.dart @@ -6,7 +6,7 @@ class SectionProvider extends ChangeNotifier { final SectionRepository _repository = SectionRepository(); List
_sections = []; bool _isLoading = false; - String? _error; + dynamic _error; List
get sections => _sections; bool get isLoading => _isLoading; diff --git a/lib/features/learning/screens/learning_screen.dart b/lib/features/learning/screens/learning_screen.dart index 6916e2c..fb643bc 100644 --- a/lib/features/learning/screens/learning_screen.dart +++ b/lib/features/learning/screens/learning_screen.dart @@ -70,7 +70,28 @@ class _LearningScreenState extends State { return _buildShimmerLoading(); } else if (sectionProvider.error != null) { return Center( - child: Text('Error: ${sectionProvider.error}')); + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Error: ${sectionProvider.error}', + style: AppTextStyles.greyTextStyle, + ), + SizedBox(height: 16), + // Tambahkan tombol retry jika diperlukan + ElevatedButton( + onPressed: _fetchSections, + child: Text('Retry'), + ) + ], + )); + } else if (sectionProvider.sections.isEmpty) { + return Center( + child: Text( + 'No sections available', + style: AppTextStyles.greyTextStyle, + ), + ); } else { return ListView.builder( itemCount: sectionProvider.sections.length, diff --git a/lib/features/learning/widgets/section_card.dart b/lib/features/learning/widgets/section_card.dart index e3c4502..683f049 100644 --- a/lib/features/learning/widgets/section_card.dart +++ b/lib/features/learning/widgets/section_card.dart @@ -23,7 +23,7 @@ class _LearningCardState extends State if (thumbnail.startsWith('http')) { return thumbnail; } else { - return '${baseUrl}uploads/section/$thumbnail'; + return '${baseUrl}api/uploads/section/$thumbnail'; } } @@ -46,7 +46,7 @@ class _LearningCardState extends State ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( - _getFullImageUrl(widget.section.thumbnail), + _getFullImageUrl(widget.section.thumbnail ?? ''), width: 90, height: 104, fit: BoxFit.cover, diff --git a/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart b/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart index 5769f47..826a676 100644 --- a/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart +++ b/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart @@ -133,7 +133,7 @@ class _EditProfileScreenState extends State { child: UserAvatar( radius: 60, pictureUrl: userProvider.userData?['PICTURE'], - baseUrl: '$baseUrl/uploads/avatar/', + baseUrl: '${baseUrl}api/uploads/avatar/', onImageSelected: (File image) { userProvider.setSelectedImage(image); }, diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart index 7e8b0be..be8db41 100644 --- a/lib/features/settings/screens/settings_screen.dart +++ b/lib/features/settings/screens/settings_screen.dart @@ -64,7 +64,7 @@ class _SettingsScreenState extends State { UserAvatar( radius: 60, pictureUrl: userProvider.userData?['PICTURE'], - baseUrl: '$baseUrl/uploads/avatar/', + baseUrl: '${baseUrl}api/uploads/avatar/', onImageSelected: (File image) { userProvider.setSelectedImage(image); }, diff --git a/workspace.tar b/workspace.tar new file mode 100644 index 0000000..c750f74 Binary files /dev/null and b/workspace.tar differ