refactor(home): improve completed topics API implementation

This commit is contained in:
Naresh Pratista 2024-10-23 11:16:07 +07:00
parent e28b60ba04
commit e1e9ad37da
46 changed files with 1510 additions and 703 deletions

View File

@ -1,3 +1,5 @@
// ignore_for_file: avoid_print
import 'package:dio/dio.dart';
import 'package:english_learning/core/services/repositories/constants.dart';
@ -10,24 +12,6 @@ class DioClient {
_dio.options.receiveTimeout = const Duration(seconds: 3);
}
Future<Response> post(String path, {dynamic data, Options? options}) async {
try {
final response = await _dio.post(
path,
data: data,
options:
options ?? Options(headers: {'Content-Type': 'application/json'}),
);
return response;
} on DioError catch (e) {
print('DioError: ${e.response?.data ?? e.message}');
rethrow; // or handle specific DioError here
} catch (e) {
print('Unexpected error: $e');
rethrow;
}
}
Future<Response> refreshAccessToken(String refreshToken) async {
try {
final response = await _dio.post(
@ -46,6 +30,24 @@ class DioClient {
}
}
Future<Response> post(String path, {dynamic data, Options? options}) async {
try {
final response = await _dio.post(
path,
data: data,
options:
options ?? Options(headers: {'Content-Type': 'application/json'}),
);
return response;
} on DioException catch (e) {
print('DioError: ${e.response?.data ?? e.message}');
rethrow;
} catch (e) {
print('Unexpected error: $e');
rethrow;
}
}
Future<Response> updateUserProfile(
String id, FormData formData, String token) async {
try {
@ -268,7 +270,7 @@ class DioClient {
);
print('getExercises response: ${response.data}');
return response;
} on DioError catch (e) {
} on DioException catch (e) {
print(
'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}');
rethrow;
@ -336,4 +338,65 @@ class DioClient {
rethrow;
}
}
Future<Response> getCompletedTopics(String token) async {
try {
final response = await _dio.get(
'/topic/complete',
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
),
);
print('getCompletedTopics response: ${response.data}');
return response;
} catch (e) {
print('GetCompletedTopics error: $e');
rethrow;
}
}
Future<Response> studentFeedback(
String stdLearningId,
String feedback,
String token,
) async {
try {
final response = await _dio.put(
'/stdLearning/$stdLearningId',
data: {'FEEDBACK_STUDENT': feedback},
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
),
);
return response;
} catch (e) {
print('Submit Feedback error: $e');
rethrow;
}
}
Future<Response> getStudentAnswers(String stdLearningId, String token) async {
try {
final response = await _dio.get(
'/studentAnswer/$stdLearningId',
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
),
);
print('studentAnswers response: ${response.data}');
return response;
} catch (e) {
print('StudentAnswers error: $e');
rethrow;
}
}
}

View File

@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import 'package:english_learning/core/services/dio_client.dart';
import 'package:english_learning/features/home/models/completed_topics_model.dart';
class CompletedTopicsRepository {
final DioClient _dioClient;
CompletedTopicsRepository(this._dioClient);
Future<List<CompletedTopic>> getCompletedTopics(String token) async {
try {
final response = await _dioClient.getCompletedTopics(token);
if (response.statusCode == 200) {
final List<dynamic> topicsData = response.data['payload'];
return topicsData.map((data) => CompletedTopic.fromJson(data)).toList();
} else {
throw Exception(
'Failed to load completed topics: ${response.statusMessage}');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: $e');
}
}
}

View File

@ -1 +1,2 @@
const String baseUrl = 'https://3311-114-6-25-184.ngrok-free.app/';
const String baseUrl =
'https://70e7-2001-448a-50a0-2604-b558-2a22-54f6-65ce.ngrok-free.app/';

View File

@ -1,4 +1,5 @@
import 'package:english_learning/core/services/dio_client.dart';
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
class ExerciseRepository {
final DioClient _dioClient;
@ -19,6 +20,20 @@ class ExerciseRepository {
}
}
Future<Map<String, dynamic>> getStudentAnswers(
String stdLearningId, String token) async {
try {
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
if (response.statusCode == 200) {
return response.data['data'];
} else {
throw Exception('Failed to load student answers');
}
} catch (e) {
throw Exception('Error fetching student answers: $e');
}
}
Future<Map<String, dynamic>> submitAnswersAndGetScore(
List<Map<String, dynamic>> answers,
String studentLearningId,
@ -38,4 +53,25 @@ class ExerciseRepository {
throw Exception('Error submitting answers and getting score: $e');
}
}
Future<FeedbackModel> submitFeedback(
String stdLearningId,
String feedback,
String token,
) async {
try {
final response = await _dioClient.studentFeedback(
stdLearningId,
feedback,
token,
);
if (response.statusCode == 200) {
return FeedbackModel.fromJson(response.data['payload']);
} else {
throw Exception('Failed to submit feedback');
}
} catch (e) {
throw Exception('Error submitting feedback: $e');
}
}
}

View File

@ -10,12 +10,12 @@ class LevelRepository {
if (response.statusCode == 200 && response.data != null) {
final Map<String, dynamic> responseData = response.data;
if (responseData.containsKey('data')) {
final Map<String, dynamic> data = responseData['data'];
final Map<String, dynamic> data = responseData['data'] ?? {};
final List<dynamic> levelsData = data['levels'] ?? [];
final List<Level> levels =
levelsData.map((json) => Level.fromJson(json)).toList();
final Map<String, dynamic>? lastCompletedLevel =
data['lastCompletedLevel'];
data['lastCompletedLevel'] ?? {};
return {
'levels': levels,
@ -32,4 +32,20 @@ class LevelRepository {
rethrow;
}
}
Future<Map<String, dynamic>> getStudentAnswers(
String stdLearningId, String token) async {
try {
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
if (response.statusCode == 200 && response.data != null) {
return response.data['data'];
} else {
throw Exception(
'Failed to fetch student answers: ${response.statusCode}');
}
} catch (e) {
throw Exception('Failed to fetch student answers: $e');
}
}
}

View File

@ -2,9 +2,9 @@ import 'package:intl/intl.dart';
class LearningHistory {
final int score;
final dynamic currentLevel;
final dynamic nextLevel;
final DateTime studentFinish;
final String currentLevel;
final String? nextLevel;
final DateTime? studentFinish;
final String topicName;
final String sectionName;
@ -19,16 +19,20 @@ class LearningHistory {
factory LearningHistory.fromJson(Map<String, dynamic> json) {
return LearningHistory(
score: json['SCORE'],
currentLevel: json['CURRENT_LEVEL'],
score: json['SCORE'] ?? 0,
currentLevel: json['CURRENT_LEVEL'] ?? 'Unknown',
nextLevel: json['NEXT_LEVEL'],
studentFinish: DateTime.parse(json['STUDENT_FINISH']),
topicName: json['TOPIC_NAME'],
sectionName: json['SECTION_NAME'],
studentFinish: json['STUDENT_FINISH'] != null
? DateTime.parse(json['STUDENT_FINISH'])
: null,
topicName: json['TOPIC_NAME'] ?? 'Unknown Topic',
sectionName: json['SECTION_NAME'] ?? 'Unknown Section',
);
}
String get formattedDate {
return DateFormat('yyyy-MM-dd HH:mm').format(studentFinish);
return studentFinish != null
? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!)
: 'N/A';
}
}

View File

@ -67,20 +67,50 @@ class HistoryProvider with ChangeNotifier {
}
}
int? _parseLevel(dynamic level) {
if (level is int) {
return level;
} else if (level is String) {
if (level.toLowerCase() == 'pretest') {
return 0; // Treat Pretest as level 0
}
return int.tryParse(level.replaceAll('Level ', ''));
int? _parseLevel(String? level) {
if (level == null) return null;
if (level.toLowerCase() == 'pretest') {
return 0; // Treat Pretest as level 0
}
return null;
return int.tryParse(level.replaceAll('Level ', '')) ?? -1;
}
Future<void> refreshData(String token) async {
await _sectionProvider.fetchSections(token);
if (_sectionProvider.sections.isEmpty) {
await _sectionProvider.fetchSections(token);
}
await fetchLearningHistory(token);
}
Future<void> loadHistoryData(String token) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
// Fetch sections and learning history in parallel
final sectionsResult = _sectionProvider.fetchSections(token);
// Use the first section ID for initial history fetch
final firstSectionId = await sectionsResult
.then((sections) => sections.isNotEmpty ? sections.first.id : null);
if (firstSectionId != null) {
final historyResult =
_repository.getLearningHistory(firstSectionId, token);
// Wait for both futures to complete
final results = await Future.wait([sectionsResult, historyResult]);
_learningHistory = results[1] as List<LearningHistory>;
} else {
_error = 'No sections available';
}
} catch (e) {
_error = 'Error loading data: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/history/provider/history_provider.dart';
@ -18,95 +19,141 @@ class HistoryScreen extends StatefulWidget {
}
class _HistoryScreenState extends State<HistoryScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context, listen: false);
historyProvider.refreshData(userProvider.jwtToken!);
});
});
bool isNotFoundError(String error) {
return error.toLowerCase().contains('no learning history found') ||
error.toLowerCase().contains('not found');
}
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final historyProvider =
// Provider.of<HistoryProvider>(context, listen: false);
// final userProvider = Provider.of<UserProvider>(context, listen: false);
// historyProvider.refreshData(userProvider.jwtToken!);
// });
// });
// }
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgSoftColor,
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 30.0,
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your Exercise History!',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
return Consumer<HistoryProvider>(
builder: (context, historyProvider, chiild) {
return Scaffold(
backgroundColor: AppColors.bgSoftColor,
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 30.0,
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your Exercise History!',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 8),
Text(
'Track your progress with a personalized overview of all your workouts.',
style: AppTextStyles.greyTextStyle.copyWith(
fontSize: 13,
fontWeight: FontWeight.w500,
const SizedBox(height: 8),
Text(
'Track your progress with a personalized overview of all your workouts.',
style: AppTextStyles.greyTextStyle.copyWith(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
),
const SizedBox(height: 8),
const CustomTabBar(),
const SizedBox(height: 8),
Expanded(
child: Consumer<HistoryProvider>(
builder: (context, historyProvider, child) {
if (historyProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
} else if (historyProvider.error != null) {
return Center(child: Text(historyProvider.error!));
} else if (historyProvider.learningHistory.isEmpty) {
return _buildEmptyState(context);
} else {
return ListView.builder(
itemCount: historyProvider.learningHistory.length,
itemBuilder: (context, index) {
return Column(
children: [
ExerciseHistoryCard(
exercise:
historyProvider.learningHistory[index],
),
const SizedBox(height: 8.0),
],
);
},
);
}
},
const SizedBox(height: 8),
const CustomTabBar(),
const SizedBox(height: 8),
Expanded(
child: Consumer<HistoryProvider>(
builder: (context, historyProvider, child) {
if (historyProvider.isLoading) {
return const Center(
child: CircularProgressIndicator());
} else if (historyProvider.error != null) {
if (isNotFoundError(historyProvider.error!)) {
return _buildEmptyState(context);
} else {
return _buildErrorState(historyProvider.error!);
}
} else if (historyProvider.learningHistory.isEmpty) {
return _buildEmptyState(context);
} else {
return ListView.builder(
itemCount: historyProvider.learningHistory.length,
itemBuilder: (context, index) {
return Column(
children: [
ExerciseHistoryCard(
exercise:
historyProvider.learningHistory[index],
),
const SizedBox(height: 8.0),
],
);
},
);
}
},
),
),
),
],
],
),
),
),
),
);
});
}
Widget _buildErrorState(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
BootstrapIcons.exclamation_diamond_fill,
color: AppColors.blueColor,
size: 82,
),
const SizedBox(height: 16),
Text(
'Error: $error',
style: AppTextStyles.redTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GlobalButton(
onPressed: () {
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider =
Provider.of<UserProvider>(context, listen: false);
historyProvider.refreshData(userProvider.jwtToken!);
},
text: 'Try Again',
),
],
),
);
}

View File

@ -5,21 +5,8 @@ import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:provider/provider.dart';
class CustomTabBar extends StatefulWidget {
const CustomTabBar({super.key});
@override
_CustomTabBarState createState() => _CustomTabBarState();
}
class _CustomTabBarState extends State<CustomTabBar> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
class CustomTabBar extends StatelessWidget {
const CustomTabBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -27,12 +14,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
final sectionProvider = Provider.of<SectionProvider>(context);
final selectedPageIndex = historyProvider.selectedPageIndex;
if (sectionProvider.sections.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: Row(
children: sectionProvider.sections.asMap().entries.map((entry) {
@ -56,12 +38,6 @@ class _CustomTabBarState extends State<CustomTabBar> {
Provider.of<HistoryProvider>(context, listen: false);
historyProvider.setSelectedPageIndex(index);
_scrollController.animateTo(
index * 100.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
final userProvider = Provider.of<UserProvider>(context, listen: false);
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
}

View File

@ -28,84 +28,71 @@ class ExerciseHistoryCard extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: '${exercise.topicName} /',
),
TextSpan(
text: ' ${exercise.sectionName}',
style: AppTextStyles.blackTextStyle)
]),
),
const SizedBox(height: 8),
RichText(
text: TextSpan(
style: AppTextStyles.greyTextStyle.copyWith(
fontSize: 12,
),
children: [
TextSpan(
text: '${exercise.currentLevel}',
style: AppTextStyles.blackTextStyle.copyWith(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '${exercise.nextLevel}',
style: AppTextStyles.blackTextStyle.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 8),
Text(
'Submission: ${exercise.formattedDate}',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
Text(
exercise.topicName,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.tetriaryTextStyle.copyWith(
fontWeight: FontWeight.w500,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 26.0,
),
decoration: BoxDecoration(
border: Border.all(
color: color,
const SizedBox(height: 8),
RichText(
text: TextSpan(
style: AppTextStyles.greyTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
),
borderRadius: BorderRadius.circular(8.0),
children: [
TextSpan(
text: '${exercise.currentLevel}',
style: AppTextStyles.blackTextStyle.copyWith(),
),
TextSpan(
text: '${exercise.nextLevel}',
style: AppTextStyles.blackTextStyle.copyWith(
color: color,
),
),
],
),
child: Text(
'${exercise.score}/100',
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Submission: ${exercise.formattedDate}',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 26.0,
),
decoration: BoxDecoration(
border: Border.all(
color: color,
),
borderRadius: BorderRadius.circular(8.0),
),
child: Text(
'${exercise.score}/100',
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
),
],
),
),

View File

@ -0,0 +1,28 @@
class CompletedTopic {
final String idSection;
final String nameSection;
final String descriptionSection;
final String thumbnail;
final int totalTopics;
final int completedTopics;
CompletedTopic({
required this.idSection,
required this.nameSection,
required this.descriptionSection,
required this.thumbnail,
required this.totalTopics,
required this.completedTopics,
});
factory CompletedTopic.fromJson(Map<String, dynamic> json) {
return CompletedTopic(
idSection: json['ID_SECTION'],
nameSection: json['NAME_SECTION'],
descriptionSection: json['DESCRIPTION_SECTION'],
thumbnail: json['THUMBNAIL'],
totalTopics: json['TOTAL_TOPICS'],
completedTopics: json['COMPLETED_TOPICS'],
);
}
}

View File

@ -0,0 +1,34 @@
// lib/features/home/providers/completed_topics_provider.dart
import 'package:english_learning/core/services/repositories/completed_topics_repository.dart';
import 'package:english_learning/features/home/models/completed_topics_model.dart';
import 'package:flutter/foundation.dart';
class CompletedTopicsProvider with ChangeNotifier {
final CompletedTopicsRepository _repository;
List<CompletedTopic> _completedTopics = [];
bool _isLoading = false;
String? _error;
CompletedTopicsProvider(this._repository);
List<CompletedTopic> get completedTopics => _completedTopics;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> fetchCompletedTopics(String token) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_completedTopics = await _repository.getCompletedTopics(token);
} catch (e) {
_error = e.toString();
print('Error fetching completed topics: $_error');
} finally {
_isLoading = false;
notifyListeners();
}
}
}

View File

@ -1,8 +1,11 @@
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:english_learning/core/widgets/custom_button.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/history/provider/history_provider.dart';
import 'package:english_learning/features/history/screens/history_screen.dart';
import 'package:english_learning/features/home/data/card_data.dart';
import 'package:english_learning/features/home/provider/completed_topics_provider.dart';
import 'package:english_learning/features/home/widgets/progress_card.dart';
import 'package:english_learning/features/home/widgets/welcome_card.dart';
import 'package:english_learning/features/learning/screens/learning_screen.dart';
@ -14,6 +17,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:google_nav_bar/google_nav_bar.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@ -78,15 +82,19 @@ class _HomeScreenState extends State<HomeScreen> {
iconSize: 20,
gap: 8,
selectedIndex: _selectedIndex,
onTabChange: (index) {
onTabChange: (index) async {
setState(() {
_selectedIndex = index;
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, // Animasi ketika berpindah tab
);
_pageController.jumpToPage(index);
});
if (index == 2) {
// Index 2 adalah tab History
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider =
Provider.of<UserProvider>(context, listen: false);
await historyProvider.loadHistoryData(userProvider.jwtToken!);
}
},
padding: const EdgeInsets.symmetric(
horizontal: 16,
@ -127,164 +135,294 @@ class HomeContent extends StatefulWidget {
class _HomeContentState extends State<HomeContent> {
final CardData cardData = CardData();
int _currentPage = 0;
bool hasOngoingExercises = false;
@override
void initState() {
super.initState();
// Memanggil fetchCompletedTopics saat HomeContent diinisialisasi
WidgetsBinding.instance.addPostFrameCallback((_) {
final userProvider = Provider.of<UserProvider>(context, listen: false);
final completedTopicsProvider =
Provider.of<CompletedTopicsProvider>(context, listen: false);
completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!);
});
}
Widget _buildNoDataWidget() {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Still new?',
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Begin your journey!',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 14,
),
),
const SizedBox(height: 24),
CustomButton(
text: 'Explore',
width: double.infinity,
height: 44,
color: AppColors.yellowButtonColor,
onPressed: () {},
),
],
),
);
}
Widget _buildShimmerEffect() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Column(
children: List.generate(
2,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Container(
width: double.infinity,
height: 150,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
)),
),
),
);
}
@override
Widget build(BuildContext context) {
return Consumer<UserProvider>(builder: (context, authProvider, child) {
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
context,
authProvider,
completedTopicsProvider,
child,
) {
final userName = authProvider.getUserName() ?? 'Guest';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: AppColors.gradientTheme,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: AppColors.gradientTheme,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
),
child: Padding(
padding: const EdgeInsets.only(
top: 60.0,
left: 18.34,
right: 16.0,
bottom: 34.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
'lib/features/home/assets/images/Logo.svg',
width: 31,
),
const SizedBox(width: 4.34),
Text(
'SEALS',
style: AppTextStyles.logoTextStyle.copyWith(
fontSize: 28,
fontWeight: FontWeight.w700,
child: Padding(
padding: const EdgeInsets.only(
top: 60.0,
left: 18.34,
right: 16.0,
bottom: 34.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
'lib/features/home/assets/images/Logo.svg',
width: 31,
),
),
],
),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const EditProfileScreen(),
const SizedBox(width: 4.34),
Text(
'SEALS',
style:
AppTextStyles.logoTextStyle.copyWith(
fontSize: 28,
fontWeight: FontWeight.w700,
),
),
);
},
child: const Icon(
BootstrapIcons.person_circle,
size: 28,
color: AppColors.whiteColor,
],
),
),
],
),
const SizedBox(height: 17),
RichText(
text: TextSpan(
text: 'Hi, ',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
children: <TextSpan>[
TextSpan(
text: userName,
style: AppTextStyles.yellowTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
TextSpan(
text: '!',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const EditProfileScreen(),
),
);
},
child: const Icon(
BootstrapIcons.person_circle,
size: 28,
color: AppColors.whiteColor,
),
),
],
),
const SizedBox(height: 17),
RichText(
text: TextSpan(
text: 'Hi, ',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
children: <TextSpan>[
TextSpan(
text: userName,
style:
AppTextStyles.yellowTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
TextSpan(
text: '!',
style:
AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
],
),
),
const SizedBox(height: 8),
Text(
'Let\'s evolve together',
style: AppTextStyles.whiteTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
),
],
),
],
),
const SizedBox(height: 16),
CarouselSlider.builder(
itemCount: cardData.cardData.length,
itemBuilder: (context, index, realIndex) {
return WelcomeCard(cardModel: cardData.cardData[index]);
},
options: CarouselOptions(
height: 168,
viewportFraction: 0.9,
enlargeCenterPage: true,
autoPlay: true,
autoPlayInterval: const Duration(seconds: 3),
autoPlayAnimationDuration: const Duration(milliseconds: 800),
autoPlayCurve: Curves.fastOutSlowIn,
onPageChanged: (index, reason) {
setState(
() {
_currentPage = index;
},
);
},
),
),
const SizedBox(height: 16),
SliderWidget(
currentPage: _currentPage,
itemCount: cardData.cardData.length,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(
top: 8.0,
left: 24.0,
right: 24.0,
bottom: 47.0,
),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Icon(
BootstrapIcons.info_circle,
color: AppColors.tetriaryColor,
size: 16,
),
const SizedBox(height: 8),
const SizedBox(width: 8),
Text(
'Let\'s evolve together',
style: AppTextStyles.whiteTextStyle.copyWith(
'Your Last Journey.',
style: AppTextStyles.tetriaryTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w400,
fontWeight: FontWeight.w800,
),
),
],
),
),
),
],
completedTopicsProvider.isLoading
? _buildShimmerEffect()
: completedTopicsProvider.completedTopics.isEmpty
? _buildNoDataWidget()
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
itemCount: 1,
itemBuilder: (context, index) {
return ProgressCard(
completedTopic: completedTopicsProvider
.completedTopics, // Kirim seluruh list
);
},
),
],
),
),
],
),
const SizedBox(height: 16),
CarouselSlider.builder(
itemCount: cardData.cardData.length,
itemBuilder: (context, index, realIndex) {
return WelcomeCard(cardModel: cardData.cardData[index]);
},
options: CarouselOptions(
height: 168,
viewportFraction: 0.9,
enlargeCenterPage: true,
autoPlay: true,
autoPlayInterval: const Duration(seconds: 3),
autoPlayAnimationDuration: const Duration(milliseconds: 800),
autoPlayCurve: Curves.fastOutSlowIn,
onPageChanged: (index, reason) {
setState(
() {
_currentPage = index;
},
);
},
),
),
const SizedBox(height: 16),
SliderWidget(
currentPage: _currentPage,
itemCount: cardData.cardData.length,
),
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: ProgressCard(),
),
// hasOngoingExercises
// ? const Padding(
// padding: EdgeInsets.symmetric(horizontal: 16.0),
// child:
// ProgressCard(), // Display progress card if exercises are completed
// )
// : const Padding(
// padding: EdgeInsets.symmetric(horizontal: 16.0),
// child:
// ExploreCard(), // Display ExploreCard if no exercises are completed
// ),
],
],
),
);
});
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
class NoProgressCard extends StatelessWidget {
const NoProgressCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.whiteColor,
),
);
}
}

View File

@ -2,31 +2,34 @@ import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
class ProgressBar extends StatelessWidget {
final int currentProgress;
final int totalProgress;
final int completedTopics;
final int totalTopics;
const ProgressBar({
super.key,
required this.currentProgress,
required this.totalProgress,
required this.completedTopics,
required this.totalTopics,
});
@override
Widget build(BuildContext context) {
final progress = totalProgress > 0 ? currentProgress / totalProgress : 0.0;
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
final progress = totalTopics > 0 ? completedTopics / totalTopics : 0.0;
return LayoutBuilder(
builder: (context, constraints) {
final barWidth = constraints.maxWidth - 40;
final barWidth = constraints.maxWidth - 30;
return Row(
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(
width: barWidth,
width: double.infinity,
child: Container(
height: 12,
decoration: BoxDecoration(
color: Colors.grey.shade300,
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(7),
),
child: Stack(
@ -44,12 +47,25 @@ class ProgressBar extends StatelessWidget {
),
),
),
const Spacer(),
Text(
'$currentProgress/$totalProgress',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.w500,
// const Spacer(),
SizedBox(
height: screenHeight * 0.02,
),
RichText(
text: TextSpan(
text: '$completedTopics/$totalTopics ',
style: AppTextStyles.blueTextStyle.copyWith(
fontWeight: FontWeight.w900,
fontSize: 12,
),
children: [
TextSpan(
text: 'Topics Completed',
style: AppTextStyles.blueTextStyle.copyWith(
fontWeight: FontWeight.w500,
),
)
],
),
),
],

View File

@ -1,91 +1,103 @@
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:english_learning/core/services/repositories/constants.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/home/models/completed_topics_model.dart';
import 'package:english_learning/features/home/widgets/progress_bar.dart';
import 'package:flutter/material.dart';
class ProgressCard extends StatelessWidget {
const ProgressCard({super.key});
final List<CompletedTopic> completedTopic;
const ProgressCard({super.key, required this.completedTopic});
String _getFullImageUrl(String thumbnail) {
return thumbnail.startsWith('http')
? thumbnail
: '${baseUrl}uploads/section/$thumbnail';
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
const Icon(
BootstrapIcons.info_circle,
color: AppColors.disableColor,
size: 16,
),
const SizedBox(width: 8),
Text(
'Your Last Journey!',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
],
),
const SizedBox(height: 24),
Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.disableColor,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Listening',
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 15,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
'Topic 8: Entertaining | Level 3',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
const ProgressBar(
currentProgress: 8,
totalProgress: 11,
),
],
),
),
),
],
),
),
)
...completedTopic.asMap().entries.map(
(entry) {
CompletedTopic topic = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildTopicItem(topic),
);
},
),
],
);
}
Widget _buildTopicItem(CompletedTopic topic) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
_getFullImageUrl(topic.thumbnail),
width: 90,
height: 130,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 90,
height: 130,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
);
},
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
topic.nameSection,
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
topic.descriptionSection,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
ProgressBar(
completedTopics: topic.completedTopics,
totalTopics: topic.totalTopics,
),
],
),
),
],
),
),
);
}
}

View File

@ -9,6 +9,8 @@ class ExerciseModel {
final String? image;
final DateTime timeAdminExc;
final dynamic choices;
final String? answerStudent;
final bool? isCorrect;
ExerciseModel({
required this.idAdminExercise,
@ -21,6 +23,8 @@ class ExerciseModel {
this.image,
required this.timeAdminExc,
required this.choices,
this.answerStudent,
this.isCorrect,
});
factory ExerciseModel.fromJson(Map<String, dynamic> json) {
@ -57,6 +61,21 @@ class ExerciseModel {
choices: choices,
);
}
factory ExerciseModel.fromReviewJson(Map<String, dynamic> json) {
final exerciseDetails = json['exerciseDetails'];
return ExerciseModel(
idAdminExercise: exerciseDetails['ID_ADMIN_EXERCISE'],
idLevel: json['ID_LEVEL'] ?? '', // This might be available in parent data
title: exerciseDetails['TITLE'],
question: exerciseDetails['QUESTION'],
questionType: exerciseDetails['QUESTION_TYPE'],
timeAdminExc: DateTime.parse(json['TIME_STUDENT_EXC']),
answerStudent: json['ANSWER_STUDENT'],
isCorrect: json['IS_CORRECT'] == 1,
choices: null,
);
}
}
class MultipleChoice {

View File

@ -1,3 +1,4 @@
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
import 'package:flutter/material.dart';
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart';
@ -23,6 +24,7 @@ class ExerciseProvider extends ChangeNotifier {
String _nameLevel = '';
String? _activeLeftOption;
String? _studentLearningId;
bool _isReview = false;
// Constants
final List<Color> _pairColors = [
@ -42,6 +44,7 @@ class ExerciseProvider extends ChangeNotifier {
String get nameLevel => _nameLevel;
String? get activeLeftOption => _activeLeftOption;
String? get studentLearningId => _studentLearningId;
bool get isReview => _isReview;
// Initialization methods
void initializeAnswers() {
@ -64,11 +67,40 @@ class ExerciseProvider extends ChangeNotifier {
});
}
// Answer handling methods
// // Answer handling methods
// void answerQuestion(int index, String answer) {
// if (index >= 0 && index < _answers.length) {
// if (_exercises[index].choices is MatchingPair) {
// _handleMatchingPairAnswer(index, answer);
// } else {
// _answers[index] = _answers[index] == answer ? '' : answer;
// }
// notifyListeners();
// }
// }
void answerQuestion(int index, String answer) {
if (_isReview) return;
if (index >= 0 && index < _answers.length) {
if (_exercises[index].choices is MatchingPair) {
_handleMatchingPairAnswer(index, answer);
} else if (_exercises[index].choices is MultipleChoice) {
// Store the letter index (e.g., "A", "B", "C", etc.)
final multipleChoice = _exercises[index].choices as MultipleChoice;
final options = [
multipleChoice.optionA,
multipleChoice.optionB,
multipleChoice.optionC,
multipleChoice.optionD,
multipleChoice.optionE,
];
final optionIndex = options.indexOf(answer);
if (optionIndex != -1) {
_answers[index] = String.fromCharCode(65 + optionIndex);
}
} else if (_exercises[index].choices is TrueFalse) {
// Store "1" for true and "0" for false
// _answers[index] = answer.toLowerCase() == 'true' ? '1' : '0';
_answers[index] = answer;
} else {
_answers[index] = _answers[index] == answer ? '' : answer;
}
@ -238,6 +270,33 @@ class ExerciseProvider extends ChangeNotifier {
}
}
Future<void> fetchReviewExercises(String stdLearningId) async {
_isLoading = true;
_isReview = true;
notifyListeners();
try {
final token = await _userProvider.getValidToken();
if (token == null) {
throw Exception('No valid token found');
}
final data = await _repository.getStudentAnswers(stdLearningId, token);
_nameTopic = data['NAME_TOPIC'];
_nameLevel = data['NAME_LEVEL'];
final exercisesData = data['stdExercises'];
_exercises = exercisesData
.map<ExerciseModel>((json) => ExerciseModel.fromReviewJson(json))
.toList();
_answers = _exercises.map((e) => e.answerStudent ?? '').toList();
} catch (e) {
print('Error fetching review exercises: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
print('submitAnswersAndGetScore called');
try {
@ -256,10 +315,17 @@ class ExerciseProvider extends ChangeNotifier {
}).map((entry) {
final index = entry.key;
final exercise = entry.value;
String formattedAnswer = _answers[index];
if (exercise.choices is MultipleChoice) {
formattedAnswer = formattedAnswer;
} else if (exercise.choices is TrueFalse) {
formattedAnswer = formattedAnswer.toLowerCase() == 'true' ? '1' : '0';
}
return {
'ID_STUDENT_LEARNING': _studentLearningId!,
'ID_ADMIN_EXERCISE': exercise.idAdminExercise,
'ANSWER_STUDENT': _answers[index],
'ANSWER_STUDENT': formattedAnswer,
};
}).toList();
@ -286,6 +352,26 @@ class ExerciseProvider extends ChangeNotifier {
}
}
Future<FeedbackModel> submitFeedback(String feedback) async {
try {
final token = await _userProvider.getValidToken();
if (token == null) {
throw Exception('No valid token found');
}
if (_studentLearningId == null) {
throw Exception('Student Learning ID is not set');
}
print('Submitting feedback for stdLearningId: $_studentLearningId');
final result = await _repository.submitFeedback(
_studentLearningId!, feedback, token);
print('Feedback submitted successfully');
return result;
} catch (e) {
print('Error submitting feedback: $e');
rethrow;
}
}
bool hasAnsweredQuestions() {
return _answers.any((answer) => answer.isNotEmpty);
}

View File

@ -10,11 +10,13 @@ import 'package:provider/provider.dart';
class ExerciseScreen extends StatefulWidget {
final String? levelId;
final String studentLearningId;
final bool isReview;
const ExerciseScreen({
super.key,
required this.levelId,
required this.studentLearningId,
this.isReview = false,
});
@override
@ -29,7 +31,11 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
_scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<ExerciseProvider>();
provider.fetchExercises(widget.levelId!);
if (widget.isReview) {
provider.fetchReviewExercises(widget.studentLearningId);
} else {
provider.fetchExercises(widget.levelId!);
}
provider.setStudentLearningId(widget.studentLearningId);
});
}
@ -98,6 +104,8 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
if (provider.isReview)
Text('Review Mode', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
const ExerciseContent(),
const SizedBox(height: 24),

View File

@ -31,10 +31,11 @@ class ExerciseNavigator extends StatelessWidget {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => ResultScreen(
currentLevel: result['CURRENT_LEVEL_NAME'],
nextLevel: result['NEXT_LEARNING_NAME'],
score: int.parse(result['SCORE'].toString()),
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
score: int.tryParse(result['SCORE'].toString()) ?? 0,
isCompleted: result['IS_PASS'] == 1,
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
),
),
);

View File

@ -1,4 +1,3 @@
// multiple_choice_question.dart
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
@ -31,7 +30,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
final optionLabels = List.generate(
options.length,
(index) => String.fromCharCode(65 + index),
(index) =>
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
);
return ListView.builder(
@ -41,7 +41,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
itemBuilder: (context, i) {
final option = options[i];
final isSelected =
provider.answers[provider.currentExerciseIndex] == option;
provider.answers[provider.currentExerciseIndex] == optionLabels[i];
return GestureDetector(
onTap: () =>
@ -69,10 +69,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14.0,
vertical: 10.0,
),
padding:
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
child: Text(
label,
style: AppTextStyles.blackTextStyle.copyWith(

View File

@ -28,12 +28,15 @@ class TrueFalseQuestion extends StatelessWidget {
itemCount: options.length,
itemBuilder: (context, i) {
final option = options[i];
final isSelected =
provider.answers[provider.currentExerciseIndex] == option;
final isSelected = provider.answers[provider.currentExerciseIndex] ==
(i == 0 ? '1' : '0');
return GestureDetector(
onTap: () =>
provider.answerQuestion(provider.currentExerciseIndex, option),
// onTap: () =>
// provider.answerQuestion(provider.currentExerciseIndex, option),
onTap: () => provider.answerQuestion(
provider.currentExerciseIndex, i == 0 ? '1' : '0'),
child: _buildOptionItem(option, isSelected),
);
},

View File

@ -0,0 +1,40 @@
class FeedbackModel {
final String idStudentLearning;
final String id;
final String idLevel;
final DateTime studentStart;
final DateTime studentFinish;
final int score;
final bool isPass;
final String nextLearning;
final String feedbackStudent;
final DateTime timeLearning;
FeedbackModel({
required this.idStudentLearning,
required this.id,
required this.idLevel,
required this.studentStart,
required this.studentFinish,
required this.score,
required this.isPass,
required this.nextLearning,
required this.feedbackStudent,
required this.timeLearning,
});
factory FeedbackModel.fromJson(Map<String, dynamic> json) {
return FeedbackModel(
idStudentLearning: json['ID_STUDENT_LEARNING'],
id: json['ID'],
idLevel: json['ID_LEVEL'],
studentStart: DateTime.parse(json['STUDENT_START']),
studentFinish: DateTime.parse(json['STUDENT_FINISH']),
score: json['SCORE'],
isPass: json['IS_PASS'] == 1,
nextLearning: json['NEXT_LEARNING'],
feedbackStudent: json['FEEDBACK_STUDENT'],
timeLearning: DateTime.parse(json['TIME_LEARNING']),
);
}
}

View File

@ -1,10 +1,17 @@
import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FeedbackScreen extends StatefulWidget {
const FeedbackScreen({super.key});
final String stdLearningId;
const FeedbackScreen({
super.key,
required this.stdLearningId,
});
@override
State<FeedbackScreen> createState() => _FeedbackScreenState();
@ -33,6 +40,58 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
_focusNode.unfocus();
}
Future<void> _submitFeedback() async {
if (_controller.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter your feedback before submitting.'),
backgroundColor: Colors.red,
),
);
return;
}
final exerciseProvider =
Provider.of<ExerciseProvider>(context, listen: false);
try {
final result = await exerciseProvider.submitFeedback(_controller.text);
print('Feedback submitted successfully: $result');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Feedback submitted successfully!'),
backgroundColor: Colors.green,
),
);
// Show the dialog
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return FeedbackDialog(
onSubmit: () {
Navigator.of(dialogContext).pop(); // Close the dialog
Navigator.of(context).pop(); // Return to the previous screen
},
);
},
);
}
} catch (e) {
print('Error submitting feedback: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to submit feedback: $e'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
@ -121,23 +180,7 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
const SizedBox(height: 24),
GlobalButton(
text: 'Send',
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return FeedbackDialog(
onSubmit: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const LevelListScreen(),
// ),
// );
},
);
},
);
},
onPressed: _submitFeedback,
),
const SizedBox(height: 16),
GlobalButton(
@ -149,9 +192,13 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const LevelListScreen(),
// builder: (context) => LevelListScreen(
// topicId: widget.topicId!,
// topicTitle: widget.topicTitle!,
// ),
// ),
// );
Navigator.of(context).pop(true);
},
)
],

View File

@ -58,6 +58,11 @@ class FeedbackDialog extends StatelessWidget {
// builder: (context) => const LevelListScreen(),
// ),
// );
onSubmit();
// Navigator.of(context).popUntil(
// (route) => route.isFirst,
// );
Navigator.of(context).pop(true);
},
)
],

View File

@ -33,17 +33,17 @@ class Level {
factory Level.fromJson(Map<String, dynamic> json) {
return Level(
idLevel: json['ID_LEVEL'],
idTopic: json['ID_TOPIC'],
idSection: json['ID_SECTION'],
nameSection: json['NAME_SECTION'],
nameTopic: json['NAME_TOPIC'],
nameLevel: json['NAME_LEVEL'],
content: json['CONTENT'],
idLevel: json['ID_LEVEL'] ?? '',
idTopic: json['ID_TOPIC'] ?? '',
idSection: json['ID_SECTION'] ?? '',
nameSection: json['NAME_SECTION'] ?? '',
nameTopic: json['NAME_TOPIC'] ?? '',
nameLevel: json['NAME_LEVEL'] ?? '',
content: json['CONTENT'] ?? '',
audio: json['AUDIO'],
image: json['IMAGE'],
video: json['VIDEO'],
isPretest: json['IS_PRETEST'],
isPretest: json['IS_PRETEST'] ?? 0,
timeLevel: json['TIME_LEVEL'],
idStudentLearning: json['ID_STUDENT_LEARNING'],
score: json['SCORE'],

View File

@ -6,13 +6,17 @@ class LevelProvider with ChangeNotifier {
final LevelRepository _levelRepository = LevelRepository();
List<Level> _levels = [];
Map<String, dynamic>? _lastCompletedLevel;
List<String> _unlockedLevels = [];
bool _isLoading = false;
String? _error;
Map<String, dynamic>? _studentAnswers;
List<Level> get levels => _levels;
Map<String, dynamic>? get lastCompletedLevel => _lastCompletedLevel;
List<String> get unlockedLevels => _unlockedLevels;
bool get isLoading => _isLoading;
String? get error => _error;
Map<String, dynamic>? get studentAnswers => _studentAnswers;
Future<void> fetchLevels(String topicId, String token) async {
_isLoading = true;
@ -23,6 +27,9 @@ class LevelProvider with ChangeNotifier {
final result = await _levelRepository.getLevels(topicId, token);
_levels = result['levels'];
_lastCompletedLevel = result['lastCompletedLevel'];
_unlockedLevels =
List<String>.from(_lastCompletedLevel?['UNLOCKED_LEVELS'] ?? []);
if (_levels.isEmpty) {
_error = 'No levels found for this topic';
}
@ -39,37 +46,90 @@ class LevelProvider with ChangeNotifier {
orElse: () => throw Exception('Pretest not found'));
}
// int getLevelScore(String levelId) {
// final level = _levels.firstWhere((level) => level.idLevel == levelId,
// orElse: () => const Level(
// idLevel: '',
// idTopic: '',
// idSection: '',
// nameSection: '',
// nameTopic: '',
// nameLevel: '',
// content: '',
// isPretest: 0,
// timeLevel: '',
// ));
// return level.score ?? 0;
// }
// bool isLevelAllowed(String levelName) {
// return _unlockedLevels.contains(levelName);
// }
// bool isLevelCompleted(String levelId) {
// return _lastCompletedLevel != null &&
// _lastCompletedLevel!['ID_LEVEL'] == levelId;
// }
Future<void> fetchStudentAnswers(String stdLearningId, String token) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_studentAnswers =
await _levelRepository.getStudentAnswers(stdLearningId, token);
} catch (e) {
_error = 'Error fetching student answers: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
}
}
bool isPretestFinished(String levelId) {
return _levels.any(
(level) => level.idLevel == levelId && level.idStudentLearning != null);
}
int getPretestScore(String levelId) {
final pretest = _levels.firstWhere(
(level) => level.idLevel == levelId && level.isPretest == 1,
orElse: () => throw Exception('Null'));
return pretest.score ?? 0;
}
int getLevelScore(String levelId) {
final level = _levels.firstWhere((level) => level.idLevel == levelId,
orElse: () => const Level(
idLevel: '',
idTopic: '',
idSection: '',
nameSection: '',
nameTopic: '',
nameLevel: '',
content: '',
isPretest: 0,
timeLevel: '',
));
orElse: () => throw Exception('Null'));
return level.score ?? 0;
}
bool isLevelAllowed(String levelId) {
if (_lastCompletedLevel == null) {
// Jika tidak ada level yang selesai, hanya pretest yang diizinkan
return levelId == _levels.first.idLevel;
}
String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL'];
int lastCompletedIndex =
_levels.indexWhere((level) => level.idLevel == lastCompletedLevelId);
int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId);
// Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1
return currentIndex <= lastCompletedIndex + 1;
return _unlockedLevels.contains(
_levels.firstWhere((level) => level.idLevel == levelId).nameLevel);
}
bool isLevelCompleted(String levelId) {
return _levels.any(
(level) => level.idLevel == levelId && level.idStudentLearning != null);
}
// bool isLevelAllowed(String levelId) {
// if (_lastCompletedLevel == null) {
// // Jika tidak ada level yang selesai, hanya pretest yang diizinkan
// return levelId == _levels.first.idLevel;
// }
// String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL'];
// int lastCompletedIndex =
// _levels.indexWhere((level) => level.idLevel == lastCompletedLevelId);
// int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId);
// // Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1
// return currentIndex <= lastCompletedIndex + 1;
// }
// bool isLevelAllowed(int levelIndex) {
// if (levelIndex == 0) return true; // Pretest is always allowed
// if (_lastCompletedLevel == null)

View File

@ -24,11 +24,13 @@ class _LevelListScreenState extends State<LevelListScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final levelProvider = Provider.of<LevelProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context, listen: false);
levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!);
});
_fetchLevels();
}
Future<void> _fetchLevels() async {
final levelProvider = Provider.of<LevelProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context, listen: false);
await levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!);
}
@override
@ -69,16 +71,23 @@ class _LevelListScreenState extends State<LevelListScreen> {
if (levelProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
} else if (levelProvider.error != null) {
return Center(child: Text('No levels available'));
return const Center(child: Text('No levels available'));
} else {
final pretest = levelProvider.getPretest();
final otherLevels = levelProvider.levels
.where((level) => level.isPretest == 0)
.toList();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
PretestCard(
pretest: levelProvider.getPretest(),
score: levelProvider
.getLevelScore(levelProvider.getPretest().idLevel),
pretest: pretest,
score: levelProvider.getPretestScore(pretest.idLevel),
isCompleted:
levelProvider.isPretestFinished(pretest.idLevel),
isAllowed: levelProvider.isLevelAllowed(pretest.idLevel),
),
const SizedBox(height: 12),
Expanded(
@ -90,14 +99,17 @@ class _LevelListScreenState extends State<LevelListScreen> {
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: levelProvider.levels.length - 1,
itemCount: otherLevels.length,
itemBuilder: (context, index) {
final level = levelProvider.levels[index + 1];
final level = otherLevels[index];
return LevelCard(
level: level,
isAllowed:
levelProvider.isLevelAllowed(level.idLevel),
score: levelProvider.getLevelScore(level.idLevel),
isCompleted:
levelProvider.isLevelCompleted(level.idLevel),
);
},
),

View File

@ -7,25 +7,19 @@ import 'package:flutter/material.dart';
class LevelCard extends StatelessWidget {
final Level level;
final bool isAllowed;
// final int level;
// final bool isAllowed;
// final bool isFinished;
final int score;
// final VoidCallback? onPressed;
final bool isCompleted;
const LevelCard({
super.key,
required this.level,
required this.isAllowed,
// required this.level,
// required this.isAllowed,
// required this.isFinished,
required this.score,
// this.onPressed,
required this.isCompleted,
});
@override
Widget build(BuildContext context) {
bool isCompleted = level.idStudentLearning != null;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -128,14 +122,18 @@ class LevelCard extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CustomButton(
text: isAllowed ? 'Learn Now' : 'Not Allowed',
text: isCompleted
? 'Finished'
: (isAllowed ? 'Learn Now' : 'Locked'),
textStyle:
isAllowed ? null : AppTextStyles.disableTextStyle,
width: double.infinity,
height: 36,
color: isAllowed
? AppColors.yellowButtonColor
: AppColors.cardButtonColor,
color: isCompleted
? Colors.green
: (isAllowed
? AppColors.yellowButtonColor
: AppColors.cardButtonColor),
onPressed: isAllowed
? () {
Navigator.push(

View File

@ -2,124 +2,160 @@ import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/custom_button.dart';
import 'package:english_learning/features/learning/modules/level/models/level_model.dart';
import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart';
import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class PretestCard extends StatelessWidget {
final Level pretest;
final int? score;
final VoidCallback? onPressed;
final bool isCompleted;
final bool isAllowed;
const PretestCard({
super.key,
required this.pretest,
this.score,
this.onPressed,
required this.isCompleted,
required this.isAllowed,
});
@override
Widget build(BuildContext context) {
bool isCompleted = pretest.idStudentLearning != null;
return Consumer<LevelProvider>(builder: (context, levelProvider, _) {
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
final score = levelProvider.getPretestScore(pretest.idLevel);
// final isAllowed = levelProvider.isLevelAllowed(pretest.idLevel);
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
gradient: AppColors.gradientTheme,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: isCompleted ? Colors.green : Colors.transparent,
border: Border.all(
color: AppColors.whiteColor,
width: 1.5,
elevation: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: AppColors.gradientTheme,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color:
isCompleted ? Colors.green : Colors.transparent,
border: Border.all(
color: AppColors.whiteColor,
width: 1.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
pretest.nameLevel,
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
fontSize: 16,
),
),
if (isCompleted) ...[
const SizedBox(width: 4),
const Icon(
BootstrapIcons.check_all,
color: AppColors.whiteColor,
size: 20,
),
],
],
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
pretest.nameLevel,
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
fontSize: 16,
),
),
if (isCompleted) ...[
const SizedBox(width: 4),
const Icon(
BootstrapIcons.check_all,
color: AppColors.whiteColor,
size: 20,
),
],
],
const SizedBox(height: 6),
Text(
'Score $score/100',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
fontSize: 12,
),
),
),
const SizedBox(height: 6),
Text(
'Score $score/100',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
fontSize: 12,
),
),
const SizedBox(height: 16),
],
),
Flexible(
child: Image.asset(
'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png',
height: 95,
fit: BoxFit.cover,
const SizedBox(height: 16),
],
),
),
],
),
const SizedBox(height: 13),
CustomButton(
text: isCompleted ? 'Finished' : 'Learn Now',
textStyle: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
Flexible(
child: Image.asset(
'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png',
height: 95,
fit: BoxFit.cover,
),
),
],
),
width: double.infinity,
height: 36,
color: isCompleted ? Colors.green : AppColors.yellowButtonColor,
onPressed: () {
if (!isCompleted) {
const SizedBox(height: 13),
CustomButton(
text: isFinished ? 'Review' : 'Learn Now',
textStyle: isFinished
? AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
)
: AppTextStyles.blackButtonTextStyle.copyWith(
fontWeight: FontWeight.w900,
),
width: double.infinity,
height: 36,
color: isFinished ? Colors.green : AppColors.yellowButtonColor,
// onPressed: () {
// if (!isCompleted) {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => MaterialScreen(
// levelId: pretest.idLevel,
// ),
// ),
// );
// }
// // Jika isCompleted true, tidak melakukan apa-apa
// },
// onPressed: isAllowed
// ? () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => MaterialScreen(
// levelId: pretest.idLevel,
// ),
// ),
// );
// }
// : () {},
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MaterialScreen(
levelId: pretest.idLevel,
isReview: isFinished,
),
),
);
}
// Jika isCompleted true, tidak melakukan apa-apa
},
),
],
},
),
],
),
),
),
);
);
});
}
}

View File

@ -14,9 +14,12 @@ import 'package:provider/provider.dart';
class MaterialScreen extends StatefulWidget {
final String levelId;
final bool isReview;
const MaterialScreen({
super.key,
required this.levelId,
this.isReview = false,
});
@override
@ -74,7 +77,7 @@ class _MaterialScreenState extends State<MaterialScreen>
print('Student Learning created: ${result['message']}');
// Navigate to ExerciseScreen
Navigator.push(
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ExerciseScreen(
@ -164,15 +167,28 @@ class _MaterialScreenState extends State<MaterialScreen>
videoUrl: level.video!,
),
const SizedBox(height: 32),
GlobalButton(
text: 'Take Pretest',
onPressed: _isLoading
? null
: () {
_stopAndResetAllMedia();
_createStudentLearning();
},
)
if (!widget.isReview)
GlobalButton(
text: level.isPretest == 1
? 'Take Pretest'
: 'Take Exercises',
onPressed: _isLoading
? null
: () {
_stopAndResetAllMedia();
_createStudentLearning();
},
)
else
GlobalButton(
text: 'Start Review',
onPressed: _isLoading
? null
: () {
_stopAndResetAllMedia();
_createStudentLearning();
},
),
],
),
),

View File

@ -20,6 +20,8 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
VideoPlayerController? _videoController;
YoutubePlayerController? _youtubeController;
FlickManager? _flickManager;
bool _isLoading = true;
String? _error;
bool get wantKeepAlive => true;
@ -29,6 +31,18 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
_initializeVideoPlayerWidget();
}
String _getPlayableUrl(String url) {
if (url.contains('drive.google.com')) {
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
final match = regex.firstMatch(url);
if (match != null) {
final fileId = match.group(1);
return 'https://drive.google.com/uc?export=download&id=$fileId';
}
}
return url;
}
void _initializeVideoPlayerWidget() {
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
_youtubeController = YoutubePlayerController(
@ -44,18 +58,32 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
showVideoProgressIndicator: true,
onReady: () {
_youtubeController!.addListener(_youtubeListener);
setState(() {
_isLoading = false;
});
},
);
} else {
_videoController =
VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
_flickManager = FlickManager(
videoPlayerController: _videoController!,
autoPlay: false,
);
_videoWidget = FlickVideoPlayer(
flickManager: _flickManager!,
_videoController = VideoPlayerController.networkUrl(
Uri.parse(_getPlayableUrl(widget.videoUrl)),
);
_videoController!.initialize().then((_) {
_flickManager = FlickManager(
videoPlayerController: _videoController!,
autoPlay: false,
);
_videoWidget = FlickVideoPlayer(
flickManager: _flickManager!,
);
setState(() {
_isLoading = false;
});
}).catchError((error) {
setState(() {
_error = "Error loading video: $error";
_isLoading = false;
});
});
}
}
@ -76,6 +104,11 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
} else if (_error != null) {
return Center(child: Text(_error!));
}
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AspectRatio(

View File

@ -8,7 +8,8 @@ class ResultScreen extends StatelessWidget {
final String currentLevel;
final String nextLevel;
final int score;
final bool? isCompleted;
final bool isCompleted;
final String? stdLearningId;
const ResultScreen({
super.key,
@ -16,6 +17,7 @@ class ResultScreen extends StatelessWidget {
required this.nextLevel,
required this.score,
required this.isCompleted,
this.stdLearningId,
});
@override
@ -30,20 +32,23 @@ class ResultScreen extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isCompleted!)
if (isCompleted)
CompleteResultWidget(
currentLevel: currentLevel,
score: score,
stdLearningId: stdLearningId ?? '',
)
else if (nextLevel != currentLevel)
JumpResultWidget(
nextLevel: nextLevel,
score: score,
stdLearningId: stdLearningId ?? '',
)
else
DownResultWidget(
nextLevel: nextLevel,
score: score,
stdLearningId: stdLearningId ?? '',
),
],
),

View File

@ -1,16 +1,19 @@
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class CompleteResultWidget extends StatelessWidget {
final String? currentLevel;
final int? score;
final String stdLearningId;
const CompleteResultWidget({
super.key,
required this.currentLevel,
required this.score,
required this.stdLearningId,
});
@override
@ -49,12 +52,14 @@ class CompleteResultWidget extends StatelessWidget {
GlobalButton(
text: 'Discover More',
onPressed: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const FeedbackScreen(),
// ),
// );
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => FeedbackScreen(
stdLearningId: stdLearningId,
),
),
);
},
)
],

View File

@ -1,15 +1,19 @@
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DownResultWidget extends StatelessWidget {
final String? nextLevel;
final int? score;
final String stdLearningId;
const DownResultWidget({
super.key,
required this.nextLevel,
required this.score,
required this.stdLearningId,
});
@override
@ -58,12 +62,14 @@ class DownResultWidget extends StatelessWidget {
GlobalButton(
text: 'Continue',
onPressed: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const FeedbackScreen(),
// ),
// );
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => FeedbackScreen(
stdLearningId: stdLearningId,
),
),
);
},
)
],

View File

@ -7,12 +7,13 @@ import 'package:flutter_svg/svg.dart';
class JumpResultWidget extends StatelessWidget {
final String? nextLevel;
final int? score;
final String stdLearningId;
const JumpResultWidget({
super.key,
required this.nextLevel,
required this.score,
});
const JumpResultWidget(
{super.key,
required this.nextLevel,
required this.score,
required this.stdLearningId});
@override
Widget build(BuildContext context) {
@ -60,10 +61,12 @@ class JumpResultWidget extends StatelessWidget {
GlobalButton(
text: 'Continue',
onPressed: () {
Navigator.push(
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const FeedbackScreen(),
builder: (context) => FeedbackScreen(
stdLearningId: stdLearningId,
),
),
);
},

View File

@ -1,22 +1,22 @@
class Topic {
final String id;
final String sectionId;
final String name;
final String description;
final bool isCompleted;
Topic({
required this.id,
required this.sectionId,
required this.name,
required this.description,
required this.isCompleted,
});
factory Topic.fromJson(Map<String, dynamic> json) {
return Topic(
id: json['ID_TOPIC'],
sectionId: json['ID_SECTION'],
name: json['NAME_TOPIC'],
description: json['DESCRIPTION_TOPIC'],
id: json['ID_TOPIC'] ?? '',
name: json['NAME_TOPIC'] ?? '',
description: json['DESCRIPTION_TOPIC'] ?? '',
isCompleted: json['IS_COMPLETED'] == 1,
);
}
}

View File

@ -23,7 +23,9 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
@override
void initState() {
super.initState();
_fetchTopics();
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchTopics();
});
}
String _getFullImageUrl(String thumbnail) {
@ -39,10 +41,14 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
final token = await userProvider.getValidToken();
if (token != null) {
await Provider.of<TopicProvider>(context, listen: false)
.fetchTopics(widget.sectionId, token);
try {
await Provider.of<TopicProvider>(context, listen: false)
.fetchTopics(widget.sectionId, token);
print('Topics fetched successfully');
} catch (e) {
print('Error fetching topics: $e');
}
} else {
// Handle the case when token is null (user might not be logged in)
print('No valid token found. User might need to log in.');
}
}
@ -100,7 +106,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
width: 90,
height: 104,
color: Colors.grey[300],
child: Center(child: CircularProgressIndicator()),
child: const Center(child: CircularProgressIndicator()),
);
},
),
@ -142,8 +148,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
return TopicCard(
title: topic.name,
description: topic.description,
isCompleted:
false, // You might want to implement completion tracking
isCompleted: topic.isCompleted,
onTap: () {
Navigator.push(
context,

View File

@ -63,7 +63,8 @@ class TopicCard extends StatelessWidget {
isCompleted
? Icons.check_circle
: Icons.radio_button_unchecked,
color: AppColors.blueColor,
color:
isCompleted ? AppColors.blueColor : AppColors.greyColor,
),
],
),

View File

@ -12,19 +12,15 @@ class SectionProvider extends ChangeNotifier {
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> fetchSections(String token) async {
_isLoading = true;
_error = null;
notifyListeners();
Future<List<Section>> fetchSections(String token) async {
try {
_sections = await _repository.getSections(token);
_isLoading = false;
notifyListeners();
return _sections;
} catch (e) {
_isLoading = false;
_error = e.toString();
notifyListeners();
return [];
}
}
}

View File

@ -50,7 +50,7 @@ class LearningCard extends StatelessWidget {
width: 90,
height: 104,
color: Colors.grey[300],
child: Icon(
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),

View File

@ -1,10 +1,12 @@
import 'package:english_learning/core/services/dio_client.dart';
import 'package:english_learning/core/services/repositories/completed_topics_repository.dart';
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
import 'package:english_learning/core/services/repositories/history_repository.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/auth/provider/validator_provider.dart';
import 'package:english_learning/features/history/provider/history_provider.dart';
import 'package:english_learning/features/home/provider/completed_topics_provider.dart';
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart';
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart';
@ -18,7 +20,7 @@ void main() {
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
const MyApp({super.key});
@override
Widget build(BuildContext context) {
@ -38,6 +40,9 @@ class MyApp extends StatelessWidget {
ProxyProvider<DioClient, HistoryRepository>(
update: (_, dioClient, __) => HistoryRepository(dioClient),
),
ProxyProvider<DioClient, CompletedTopicsRepository>(
update: (_, dioClient, __) => CompletedTopicsRepository(dioClient),
),
ChangeNotifierProxyProvider2<HistoryRepository, SectionProvider,
HistoryProvider>(
create: (context) => HistoryProvider(
@ -62,6 +67,14 @@ class MyApp extends StatelessWidget {
userProvider,
)..updateFrom(previous),
),
ChangeNotifierProxyProvider<CompletedTopicsRepository,
CompletedTopicsProvider>(
create: (context) => CompletedTopicsProvider(
context.read<CompletedTopicsRepository>(),
),
update: (context, completedTopicsRepository, previous) =>
CompletedTopicsProvider(completedTopicsRepository),
),
],
child: Consumer<UserProvider>(
builder: (context, userProvider, _) {

View File

@ -287,7 +287,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: "direct main"
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
@ -358,14 +358,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_logger_plus:
dependency: "direct main"
description:
name: flutter_logger_plus
sha256: c01751b8074384d116a4c1e4b549233053ea6b78fcc07b5503248227b06b923c
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -600,14 +592,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logger:
dependency: "direct main"
description:
name: logger
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
matcher:
dependency: transitive
description:

View File

@ -37,8 +37,6 @@ dependencies:
flick_video_player: ^0.9.0
flutter:
sdk: flutter
flutter_cache_manager: ^3.4.1
flutter_logger_plus: ^5.0.0
flutter_secure_storage: ^9.2.2
flutter_svg: ^2.0.10+1
google_fonts: ^6.2.1
@ -46,7 +44,6 @@ dependencies:
image_picker: ^1.1.2
intl: ^0.19.0
jwt_decoder: ^2.0.1
logger: ^2.4.0
provider: ^6.1.2
shared_preferences: ^2.3.2
shimmer: ^3.0.0