Updated various files, including constants, exercise providers, question widgets, section models, and screens for topics, learning, and settings, with changes to URLs, error handling, and UI components.

This commit is contained in:
Naresh Pratista 2024-11-21 12:45:41 +07:00
parent 10b2c8010d
commit 7b04d304aa
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.