Merge branch 'various-fix' into 'master'

Updated various files, including constants, exercise providers, question...

See merge request profile-image/kedaireka/polinema-adapative-learning/mobile-adaptive-learning!15
This commit is contained in:
Naresh Pratista 2024-11-21 05:47:39 +00:00
commit e500ed0fa0
15 changed files with 373 additions and 149 deletions

View File

@ -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/';

View File

@ -9,7 +9,7 @@ class DioClient {
DioClient() { DioClient() {
_dio.options.baseUrl = baseUrl; _dio.options.baseUrl = baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 5); _dio.options.connectTimeout = const Duration(seconds: 5);
_dio.options.receiveTimeout = const Duration(seconds: 3); _dio.options.receiveTimeout = const Duration(seconds: 10);
} }
Future<Response> refreshAccessToken(String refreshToken) async { Future<Response> refreshAccessToken(String refreshToken) async {

View File

@ -439,8 +439,7 @@ class _HomeContentState extends State<HomeContent> {
), ),
completedTopicsProvider.isLoading completedTopicsProvider.isLoading
? _buildShimmerEffect() ? _buildShimmerEffect()
: completedTopicsProvider.completedTopics == null || : completedTopicsProvider.completedTopics.isEmpty
completedTopicsProvider.completedTopics.isEmpty
? _buildNoDataWidget() ? _buildNoDataWidget()
: _buildCompletedTopicsContent( : _buildCompletedTopicsContent(
completedTopicsProvider), completedTopicsProvider),

View File

@ -16,7 +16,7 @@ class ProgressCard extends StatelessWidget {
String _getFullImageUrl(String thumbnail) { String _getFullImageUrl(String thumbnail) {
return thumbnail.startsWith('http') return thumbnail.startsWith('http')
? thumbnail ? thumbnail
: '${baseUrl}uploads/section/$thumbnail'; : '${baseUrl}api/uploads/section/$thumbnail';
} }
Future<void> _navigateToTopics( Future<void> _navigateToTopics(

View File

@ -27,6 +27,8 @@ class ExerciseProvider extends ChangeNotifier {
String _nameLevel = ''; String _nameLevel = '';
String? _activeLeftOption; String? _activeLeftOption;
String? _studentLearningId; String? _studentLearningId;
List<List<dynamic>> _shuffledRightPairsList = [];
List<List<dynamic>> _savedShuffledRightPairsList = [];
// Constants // Constants
final List<Color> _pairColors = [ final List<Color> _pairColors = [
@ -47,6 +49,9 @@ class ExerciseProvider extends ChangeNotifier {
String? get activeLeftOption => _activeLeftOption; String? get activeLeftOption => _activeLeftOption;
String? get studentLearningId => _studentLearningId; String? get studentLearningId => _studentLearningId;
List<ReviewExerciseDetail> get reviewExercises => _reviewExercises; List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
List<List<dynamic>> get shuffledRightPairsList => _shuffledRightPairsList;
List<List<dynamic>> get savedShuffledRightPairsList =>
_savedShuffledRightPairsList;
// Initialization methods // Initialization methods
void initializeAnswers() { 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<ExerciseModel> 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 // // Answer handling methods
// void answerQuestion(int index, String answer) { // void answerQuestion(int index, String answer) {
// if (index >= 0 && index < _answers.length) { // 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) { void _handleMatchingPairAnswer(int exerciseIndex, String option) {
final matchingPair = _exercises[exerciseIndex].choices as MatchingPair; final matchingPair = _exercises[exerciseIndex].choices as MatchingPair;
final isLeft = matchingPair.pairs.any((pair) => pair.left == option); final isLeft = matchingPair.pairs.any((pair) => pair.left == option);
@ -137,27 +185,20 @@ class ExerciseProvider extends ChangeNotifier {
return; return;
} }
final pairs = (_exercises[exerciseIndex].choices as MatchingPair).pairs; if (!_matchingAnswers.containsKey(exerciseIndex)) {
final leftIndex = _matchingAnswers[exerciseIndex] = [];
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;
} }
// 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) { void _updateMatchingPairAnswer(int exerciseIndex) {
@ -260,11 +301,16 @@ class ExerciseProvider extends ChangeNotifier {
// API methods // API methods
Future<void> fetchExercises(String levelId) async { Future<void> fetchExercises(String levelId) async {
if (_isLoading) return; // Hindari multiple calls
_isLoading = true; _isLoading = true;
_currentExerciseIndex = 0; _currentExerciseIndex = 0;
_resetReviewState(); _resetReviewState();
notifyListeners(); notifyListeners();
// Reset shuffled pairs
_shuffledRightPairsList = [];
try { try {
final token = await _userProvider.getValidToken(); final token = await _userProvider.getValidToken();
if (token == null) { if (token == null) {
@ -275,9 +321,16 @@ class ExerciseProvider extends ChangeNotifier {
_nameTopic = data['NAME_TOPIC']; _nameTopic = data['NAME_TOPIC'];
_nameLevel = data['NAME_LEVEL']; _nameLevel = data['NAME_LEVEL'];
final exercisesData = data['EXERCISES']; final exercisesData = data['EXERCISES'];
// Map exercises
_exercises = exercisesData _exercises = exercisesData
.map<ExerciseModel>((json) => ExerciseModel.fromJson(json)) .map<ExerciseModel>((json) => ExerciseModel.fromJson(json))
.toList(); .toList();
// Inisialisasi pengacakan untuk matching pairs
initializeShuffledRightPairs(_exercises);
// Inisialisasi jawaban
_answers = List.generate(_exercises.length, (index) => ''); _answers = List.generate(_exercises.length, (index) => '');
initializeAnswers(); initializeAnswers();
} catch (e) { } catch (e) {
@ -305,6 +358,8 @@ class ExerciseProvider extends ChangeNotifier {
final payload = response['payload']; final payload = response['payload'];
_reviewData = ReviewExerciseModel.fromJson(payload); _reviewData = ReviewExerciseModel.fromJson(payload);
restoreShuffledRightPairs();
// Sort the exercises based on the title number // Sort the exercises based on the title number
_reviewExercises = (_reviewData?.stdExercises ?? []) _reviewExercises = (_reviewData?.stdExercises ?? [])
..sort((a, b) { ..sort((a, b) {
@ -376,7 +431,8 @@ class ExerciseProvider extends ChangeNotifier {
} }
Future<Map<String, dynamic>> submitAnswersAndGetScore() async { Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
print('submitAnswersAndGetScore called'); // Simpan urutan acak sebelum submit
saveShuffledRightPairs();
try { try {
if (_studentLearningId == null) { if (_studentLearningId == null) {
throw Exception('Student Learning ID is not set'); throw Exception('Student Learning ID is not set');

View File

@ -168,7 +168,7 @@ class _ExerciseContentState extends State<ExerciseContent>
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ImageWidget( child: ImageWidget(
imageFileName: widget.exercise.image!, imageFileName: widget.exercise.image!,
baseUrl: '${baseUrl}uploads/exercise/image/', baseUrl: '${baseUrl}api/uploads/exercise/image/',
), ),
), ),
), ),
@ -180,7 +180,7 @@ class _ExerciseContentState extends State<ExerciseContent>
child: AudioPlayerWidget( child: AudioPlayerWidget(
key: _audioPlayerKey, key: _audioPlayerKey,
audioFileName: widget.exercise.audio!, audioFileName: widget.exercise.audio!,
baseUrl: '${baseUrl}uploads/exercise/audio/', baseUrl: '${baseUrl}api/uploads/exercise/audio/',
), ),
), ),
), ),

View File

@ -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:english_learning/features/learning/modules/exercises/models/exercise_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MatchingPairsQuestion extends StatelessWidget { class MatchingPairsQuestion extends StatefulWidget {
final dynamic exercise; final dynamic exercise;
final bool isReview; final bool isReview;
@ -17,108 +17,155 @@ class MatchingPairsQuestion extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { State<MatchingPairsQuestion> createState() => _MatchingPairsQuestionState();
final provider = Provider.of<ExerciseProvider>(context); }
List<dynamic> pairs = [];
Map<String, String> studentAnswers = {};
final currentIndex = provider.currentExerciseIndex;
if (exercise is ExerciseModel) { class _MatchingPairsQuestionState extends State<MatchingPairsQuestion> {
pairs = (exercise.choices as MatchingPair).pairs; late List<dynamic> pairs;
} else if (exercise is ReviewExerciseDetail) { late List<String> originalRightPairs;
if (exercise.matchingPairs != null) { late List<String> shuffledRightPairs;
pairs = exercise.matchingPairs!.map((reviewPair) { late Map<String, String> studentAnswers;
late Map<String, String> originalPairMapping;
late Map<String, int> 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<ExerciseProvider>(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<String>.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<ExerciseProvider>(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<String>.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( return Pair(
left: reviewPair.leftPair, left: reviewPair.leftPair,
right: reviewPair.rightPair, right: reviewPair.rightPair,
); );
}).toList(); }).toList();
// Parse student answers // Simpan indeks asli untuk setiap pasangan
if (exercise.answerStudent.isNotEmpty) { for (int i = 0; i < pairs.length; i++) {
final answerPairs = exercise.answerStudent.split(', '); originalPairIndices[pairs[i].left] = i;
for (var pair in answerPairs) { }
final parts = pair.split('-');
if (parts.length == 2) { // Use the original right pairs without shuffling
studentAnswers[parts[0]] = parts[1]; 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 // Bangun pemetaan konsisten
Map<String, String> reverseStudentAnswers = {}; for (int i = 0; i < pairs.length; i++) {
studentAnswers.forEach((left, right) { final left = pairs[i].left;
reverseStudentAnswers[right] = left; final right = pairs[i].right;
}); originalPairMapping[left] = right;
}
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(),
],
);
} }
Color _getPairColor(int index) { Color _getPairColor(int index) {
@ -137,33 +184,84 @@ class MatchingPairsQuestion extends StatelessWidget {
String option, String option,
ExerciseProvider provider, ExerciseProvider provider,
int exerciseIndex, int exerciseIndex,
int pairIndex,
bool isLeft, bool isLeft,
bool isMatched,
Map<String, String> studentAnswers, Map<String, String> studentAnswers,
Color pairColor,
) { ) {
Color? color;
bool isSelected = false; bool isSelected = false;
bool isActive = false; bool isActive = false;
Color? color;
if (isReview) { // Mencari indeks asli pasangan untuk warna yang konsisten
if (isMatched) { int originalPairIndex = -1;
color = pairColor;
isSelected = true; 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 { } else {
if (isLeft) {
color = provider.getLeftColor(exerciseIndex, pairIndex);
isActive = provider.activeLeftOption == option;
} else {
color = provider.getRightColor(exerciseIndex, pairIndex);
}
isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft); isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
if (isSelected || isActive) {
color = _getPairColor(originalPairIndex);
}
} }
return GestureDetector( return GestureDetector(
onTap: isReview onTap: widget.isReview
? null ? null
: () { : () {
if (!isLeft && provider.activeLeftOption == null) { if (!isLeft && provider.activeLeftOption == null) {
@ -179,8 +277,10 @@ class MatchingPairsQuestion extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? color ?? pairColor ? color ?? _getPairColor(originalPairIndex)
: (isActive ? pairColor : AppColors.whiteColor), : (isActive
? _getPairColor(originalPairIndex)
: AppColors.whiteColor),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(20), topRight: Radius.circular(20),
bottomRight: Radius.circular(20), bottomRight: Radius.circular(20),
@ -192,10 +292,10 @@ class MatchingPairsQuestion extends StatelessWidget {
: AppColors.cardDisabledColor, : AppColors.cardDisabledColor,
width: isSelected ? 2 : 1, width: isSelected ? 2 : 1,
), ),
boxShadow: isActive && !isReview boxShadow: isActive && !widget.isReview
? [ ? [
BoxShadow( BoxShadow(
color: pairColor.withOpacity(0.5), color: _getPairColor(originalPairIndex).withOpacity(0.5),
spreadRadius: 1, spreadRadius: 1,
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
@ -216,4 +316,52 @@ class MatchingPairsQuestion extends StatelessWidget {
), ),
); );
} }
@override
Widget build(BuildContext context) {
final provider = Provider.of<ExerciseProvider>(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(),
],
);
}
} }

View File

@ -2,14 +2,14 @@ class Section {
final String id; final String id;
final String name; final String name;
final String description; final String description;
final String thumbnail; final String? thumbnail;
final DateTime timeSection; final DateTime timeSection;
Section({ Section({
required this.id, required this.id,
required this.name, required this.name,
required this.description, required this.description,
required this.thumbnail, this.thumbnail,
required this.timeSection, required this.timeSection,
}); });

View File

@ -32,7 +32,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
if (thumbnail.startsWith('http')) { if (thumbnail.startsWith('http')) {
return thumbnail; return thumbnail;
} else { } else {
return '${baseUrl}uploads/section/$thumbnail'; return '${baseUrl}api/uploads/section/$thumbnail';
} }
} }
@ -84,7 +84,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
Stack( Stack(
children: [ children: [
Image.network( Image.network(
_getFullImageUrl(selectedSection.thumbnail), _getFullImageUrl(selectedSection.thumbnail ?? ''),
fit: BoxFit.cover, fit: BoxFit.cover,
width: double.infinity, width: double.infinity,
height: 115, height: 115,

View File

@ -6,7 +6,7 @@ class SectionProvider extends ChangeNotifier {
final SectionRepository _repository = SectionRepository(); final SectionRepository _repository = SectionRepository();
List<Section> _sections = []; List<Section> _sections = [];
bool _isLoading = false; bool _isLoading = false;
String? _error; dynamic _error;
List<Section> get sections => _sections; List<Section> get sections => _sections;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;

View File

@ -70,7 +70,28 @@ class _LearningScreenState extends State<LearningScreen> {
return _buildShimmerLoading(); return _buildShimmerLoading();
} else if (sectionProvider.error != null) { } else if (sectionProvider.error != null) {
return Center( 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 { } else {
return ListView.builder( return ListView.builder(
itemCount: sectionProvider.sections.length, itemCount: sectionProvider.sections.length,

View File

@ -23,7 +23,7 @@ class _LearningCardState extends State<LearningCard>
if (thumbnail.startsWith('http')) { if (thumbnail.startsWith('http')) {
return thumbnail; return thumbnail;
} else { } else {
return '${baseUrl}uploads/section/$thumbnail'; return '${baseUrl}api/uploads/section/$thumbnail';
} }
} }
@ -46,7 +46,7 @@ class _LearningCardState extends State<LearningCard>
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Image.network( child: Image.network(
_getFullImageUrl(widget.section.thumbnail), _getFullImageUrl(widget.section.thumbnail ?? ''),
width: 90, width: 90,
height: 104, height: 104,
fit: BoxFit.cover, fit: BoxFit.cover,

View File

@ -133,7 +133,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
child: UserAvatar( child: UserAvatar(
radius: 60, radius: 60,
pictureUrl: userProvider.userData?['PICTURE'], pictureUrl: userProvider.userData?['PICTURE'],
baseUrl: '$baseUrl/uploads/avatar/', baseUrl: '${baseUrl}api/uploads/avatar/',
onImageSelected: (File image) { onImageSelected: (File image) {
userProvider.setSelectedImage(image); userProvider.setSelectedImage(image);
}, },

View File

@ -64,7 +64,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
UserAvatar( UserAvatar(
radius: 60, radius: 60,
pictureUrl: userProvider.userData?['PICTURE'], pictureUrl: userProvider.userData?['PICTURE'],
baseUrl: '$baseUrl/uploads/avatar/', baseUrl: '${baseUrl}api/uploads/avatar/',
onImageSelected: (File image) { onImageSelected: (File image) {
userProvider.setSelectedImage(image); userProvider.setSelectedImage(image);
}, },

BIN
workspace.tar Normal file

Binary file not shown.