diff --git a/.vscode/settings.json b/.vscode/settings.json index 8350aeb..a117bee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.27.0-0.2.pre" -} \ No newline at end of file + "dart.flutterSdkPath": "C:\\tools\\flutter" +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 82dcef6..a1b7aa0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,11 +1,12 @@ - + + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> refreshAccessToken(String refreshToken) async { diff --git a/lib/core/widgets/back_handler.dart b/lib/core/widgets/back_handler.dart new file mode 100644 index 0000000..7f48bf6 --- /dev/null +++ b/lib/core/widgets/back_handler.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +mixin BackHandlerMixin on State { + DateTime? currentBackPressTime; + + Future onWillPop() async { + DateTime now = DateTime.now(); + + if (currentBackPressTime == null || + now.difference(currentBackPressTime!) > const Duration(seconds: 2)) { + currentBackPressTime = now; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Press back again to exit'), + duration: Duration(seconds: 2), + backgroundColor: Colors.black87, + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ); + + return false; + } + return true; + } + + Widget wrapWithBackHandler({required Widget child}) { + return WillPopScope( + onWillPop: onWillPop, + child: child, + ); + } +} diff --git a/lib/core/widgets/custom_snackbar.dart b/lib/core/widgets/custom_snackbar.dart index c2a48a3..ca8310d 100644 --- a/lib/core/widgets/custom_snackbar.dart +++ b/lib/core/widgets/custom_snackbar.dart @@ -19,7 +19,7 @@ class CustomSnackBar { behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(16), duration: duration, - dismissDirection: DismissDirection.horizontal, + dismissDirection: DismissDirection.vertical, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -46,7 +46,7 @@ class CustomSnackBar { behavior: behavior, margin: const EdgeInsets.all(16), duration: duration, - dismissDirection: DismissDirection.horizontal, + dismissDirection: DismissDirection.vertical, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), diff --git a/lib/features/history/models/history_model.dart b/lib/features/history/models/history_model.dart index e59090a..51cd859 100644 --- a/lib/features/history/models/history_model.dart +++ b/lib/features/history/models/history_model.dart @@ -37,8 +37,11 @@ class HistoryModel { } String get formattedDate { - return studentFinish != null - ? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!) - : 'N/A'; + if (studentFinish == null) { + return 'N/A'; + } + + final wibDateTime = studentFinish!.add(const Duration(hours: 7)); + return '${DateFormat('yyyy-MM-dd HH:mm').format(wibDateTime)} WIB'; } } diff --git a/lib/features/history/screens/history_screen.dart b/lib/features/history/screens/history_screen.dart index dbc79e2..25ec9d1 100644 --- a/lib/features/history/screens/history_screen.dart +++ b/lib/features/history/screens/history_screen.dart @@ -62,7 +62,6 @@ class _HistoryScreenState extends State { } } - // Tambahkan method untuk shimmer loading Widget _buildShimmerLoading() { return ListView.builder( itemCount: 5, @@ -130,24 +129,20 @@ class _HistoryScreenState extends State { } Widget _buildContent(BuildContext context, HistoryProvider historyProvider) { - // Prioritaskan initial loading if (_isInitialLoading || historyProvider.isLoading) { return _buildShimmerLoading(); } - // Tangani error if (historyProvider.error != null) { return isNotFoundError(historyProvider.error!) ? _buildEmptyState(context) : _buildErrorState(context, historyProvider.error!); } - // Tampilkan empty state jika tidak ada history if (historyProvider.historyModel.isEmpty) { return _buildEmptyState(context); } - // Tampilkan daftar history dengan refresh indicator return RefreshIndicator( onRefresh: _refreshHistory, child: AnimatedSwitcher( @@ -239,46 +234,54 @@ class _HistoryScreenState extends State { } Widget _buildEmptyState(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: AppColors.whiteColor, - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Still New?', - style: AppTextStyles.blackTextStyle.copyWith( - fontSize: 18, - fontWeight: FontWeight.bold, + return RefreshIndicator( + onRefresh: _refreshHistory, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: AppColors.whiteColor, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Still New?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Begin your journey!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/history/assets/images/is_empty_illustration.svg', + width: 160, + ), + const SizedBox(height: 32), + GlobalButton( + text: 'Explore', + backgroundColor: AppColors.yellowButtonColor, + textColor: AppColors.blackColor, + onPressed: () { + HomeScreen.navigateToTab(context, 1); + }, + ), + ], ), ), - Text( - 'Begin your journey!', - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 16), - SvgPicture.asset( - 'lib/features/history/assets/images/is_empty_illustration.svg', - width: 160, - ), - const SizedBox(height: 32), - GlobalButton( - text: 'Explore', - backgroundColor: AppColors.yellowButtonColor, - textColor: AppColors.blackColor, - onPressed: () { - HomeScreen.navigateToTab(context, 1); - }, - ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 178ba2b..7124794 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -1,5 +1,6 @@ import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:english_learning/core/widgets/back_handler.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'; @@ -39,7 +40,7 @@ class HomeScreen extends StatefulWidget { } } -class _HomeScreenState extends State { +class _HomeScreenState extends State with BackHandlerMixin { final PageController _pageController = PageController(); int _selectedIndex = 0; @@ -59,83 +60,84 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - return DefaultTabController( - length: 4, - child: Scaffold( - body: Container( - color: AppColors.bgSoftColor, - child: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _pageController, - children: _screens, - onPageChanged: (index) { - setState(() { - _selectedIndex = index; - }); - }, - ), - ), - bottomNavigationBar: Container( - decoration: BoxDecoration( - gradient: AppColors.gradientTheme, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Padding( - padding: const EdgeInsets.only( - top: 20, - bottom: 24, - left: 16, - right: 16, - ), - child: GNav( - activeColor: AppColors.blueColor, - tabBackgroundColor: AppColors.whiteColor, - tabBorderRadius: 100, - color: AppColors.whiteColor, - iconSize: 20, - gap: 8, - selectedIndex: _selectedIndex, - onTabChange: (index) async { - if (index == 2 && _selectedIndex != 2) { - // Only if switching TO history tab - final historyProvider = - Provider.of(context, listen: false); - final userProvider = - Provider.of(context, listen: false); - - if (!historyProvider.isInitialized) { - await historyProvider - .loadInitialData(userProvider.jwtToken!); - } - } - - navigateToTab(index); + return wrapWithBackHandler( + child: DefaultTabController( + length: 4, + child: Scaffold( + body: Container( + color: AppColors.bgSoftColor, + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: _screens, + onPageChanged: (index) { + setState(() { + _selectedIndex = index; + }); }, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + ), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), - tabs: const [ - GButton( - icon: BootstrapIcons.house, - text: 'Home', + ), + child: Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 24, + left: 16, + right: 16, + ), + child: GNav( + activeColor: AppColors.blueColor, + tabBackgroundColor: AppColors.whiteColor, + tabBorderRadius: 100, + color: AppColors.whiteColor, + iconSize: 20, + gap: 8, + selectedIndex: _selectedIndex, + onTabChange: (index) async { + if (index == 2 && _selectedIndex != 2) { + final historyProvider = + Provider.of(context, listen: false); + final userProvider = + Provider.of(context, listen: false); + + if (!historyProvider.isInitialized) { + await historyProvider + .loadInitialData(userProvider.jwtToken!); + } + } + + navigateToTab(index); + }, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), - GButton( - icon: BootstrapIcons.book, - text: 'Learning', - ), - GButton( - icon: BootstrapIcons.clock_history, - text: 'History', - ), - GButton( - icon: BootstrapIcons.gear, - text: 'Settings', - ), - ], + tabs: const [ + GButton( + icon: BootstrapIcons.house, + text: 'Home', + ), + GButton( + icon: BootstrapIcons.book, + text: 'Learning', + ), + GButton( + icon: BootstrapIcons.clock_history, + text: 'History', + ), + GButton( + icon: BootstrapIcons.gear, + text: 'Settings', + ), + ], + ), ), ), ), @@ -169,10 +171,8 @@ class _HomeContentState extends State { Provider.of(context, listen: false); try { - // Reset data sebelum fetch completedTopicsProvider.resetData(); - // Fetch completed topics await completedTopicsProvider .fetchCompletedTopics(userProvider.jwtToken!); } catch (e) { diff --git a/lib/features/learning/modules/result/assets/images/result_stay_illustration.svg b/lib/features/learning/modules/result/assets/images/result_stay_illustration.svg new file mode 100644 index 0000000..369c4b5 --- /dev/null +++ b/lib/features/learning/modules/result/assets/images/result_stay_illustration.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/result/screens/result_screen.dart b/lib/features/learning/modules/result/screens/result_screen.dart index d8c294f..60aaad0 100644 --- a/lib/features/learning/modules/result/screens/result_screen.dart +++ b/lib/features/learning/modules/result/screens/result_screen.dart @@ -1,6 +1,7 @@ import 'package:english_learning/features/learning/modules/result/widgets/complete_result_widget.dart'; import 'package:english_learning/features/learning/modules/result/widgets/down_result_widget.dart'; import 'package:english_learning/features/learning/modules/result/widgets/jump_result_widget.dart'; +import 'package:english_learning/features/learning/modules/result/widgets/stay_result_widget.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; @@ -26,8 +27,49 @@ class ResultScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // final mediaQuery = MediaQuery.of(context); - // final screenHeight = mediaQuery.size.height; + Widget buildResultWidget() { + if (isCompleted) { + return CompleteResultWidget( + currentLevel: currentLevel, + score: score, + stdLearningId: stdLearningId ?? '', + topicId: topicId, + topicTitle: topicTitle, + ); + } + + if (currentLevel == nextLevel) { + return StayResultWidget( + nextLevel: nextLevel, + score: score, + stdLearningId: stdLearningId ?? '', + topicId: topicId, + topicTitle: topicTitle, + ); + } + + // Menggunakan compareTo untuk membandingkan level + final currentLevelNum = _extractLevelNumber(currentLevel); + final nextLevelNum = _extractLevelNumber(nextLevel); + + if (currentLevelNum < nextLevelNum) { + return JumpResultWidget( + nextLevel: nextLevel, + score: score, + stdLearningId: stdLearningId ?? '', + topicId: topicId, + topicTitle: topicTitle, + ); + } else { + return DownResultWidget( + nextLevel: nextLevel, + score: score, + stdLearningId: stdLearningId ?? '', + topicId: topicId, + topicTitle: topicTitle, + ); + } + } return Scaffold( backgroundColor: AppColors.bgSoftColor, @@ -36,33 +78,19 @@ class ResultScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (isCompleted) - CompleteResultWidget( - currentLevel: currentLevel, - score: score, - stdLearningId: stdLearningId ?? '', - topicId: topicId, // Tambahkan ini - topicTitle: topicTitle, // Tambahkan ini - ) - else if (nextLevel != currentLevel) - JumpResultWidget( - nextLevel: nextLevel, - score: score, - stdLearningId: stdLearningId ?? '', - topicId: topicId, // Tambahkan ini - topicTitle: topicTitle, // Tambahkan ini - ) - else - DownResultWidget( - nextLevel: nextLevel, - score: score, - stdLearningId: stdLearningId ?? '', - topicId: topicId, // Tambahkan ini - topicTitle: topicTitle, // Tambahkan ini - ), + buildResultWidget(), ], ), ), ); } + + int _extractLevelNumber(String level) { + final regex = RegExp(r'\d+'); + final match = regex.firstMatch(level); + if (match != null) { + return int.parse(match.group(0)!); + } + return 0; + } } diff --git a/lib/features/learning/modules/result/widgets/complete_result_widget.dart b/lib/features/learning/modules/result/widgets/complete_result_widget.dart index 347be09..bc6db91 100644 --- a/lib/features/learning/modules/result/widgets/complete_result_widget.dart +++ b/lib/features/learning/modules/result/widgets/complete_result_widget.dart @@ -38,23 +38,16 @@ class CompleteResultWidget extends StatelessWidget { ), const SizedBox(height: 24), Text( - 'Congratulations!', - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 12), - Text( - 'Way to go! You conquered LEVEL $currentLevel with a $score/100! You\'re a rock star!', - style: AppTextStyles.disableTextStyle.copyWith( + 'You conquered LEVEL $currentLevel with a $score/100! You\'re a rock star!', + style: AppTextStyles.tetriaryTextStyle.copyWith( fontSize: 14, fontWeight: FontWeight.w400, ), + textAlign: TextAlign.center, ), const SizedBox(height: 24), GlobalButton( - text: 'Discover More', + text: 'Topic Finished!', onPressed: () { Navigator.pushReplacement( context, diff --git a/lib/features/learning/modules/result/widgets/down_result_widget.dart b/lib/features/learning/modules/result/widgets/down_result_widget.dart index b6d1bff..9b57cc9 100644 --- a/lib/features/learning/modules/result/widgets/down_result_widget.dart +++ b/lib/features/learning/modules/result/widgets/down_result_widget.dart @@ -55,12 +55,15 @@ class DownResultWidget extends StatelessWidget { fontSize: 14, fontWeight: FontWeight.w400, ), + textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( '$nextLevel', - style: AppTextStyles.redTextStyle - .copyWith(fontSize: 20, fontWeight: FontWeight.w900), + style: AppTextStyles.redTextStyle.copyWith( + fontSize: 20, + fontWeight: FontWeight.w900, + ), ), const SizedBox(height: 24), GlobalButton( @@ -71,8 +74,8 @@ class DownResultWidget extends StatelessWidget { MaterialPageRoute( builder: (context) => FeedbackScreen( stdLearningId: stdLearningId, - topicId: topicId, // Tambahkan ini - topicTitle: topicTitle, // Tambahkan ini + topicId: topicId, + topicTitle: topicTitle, ), ), ); diff --git a/lib/features/learning/modules/result/widgets/jump_result_widget.dart b/lib/features/learning/modules/result/widgets/jump_result_widget.dart index 04bb499..8265517 100644 --- a/lib/features/learning/modules/result/widgets/jump_result_widget.dart +++ b/lib/features/learning/modules/result/widgets/jump_result_widget.dart @@ -51,10 +51,11 @@ class JumpResultWidget extends StatelessWidget { const SizedBox(height: 24), Text( 'Great job! You can jump to ...', - style: AppTextStyles.disableTextStyle.copyWith( + style: AppTextStyles.tetriaryTextStyle.copyWith( fontSize: 14, fontWeight: FontWeight.w400, ), + textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( @@ -71,8 +72,8 @@ class JumpResultWidget extends StatelessWidget { MaterialPageRoute( builder: (context) => FeedbackScreen( stdLearningId: stdLearningId, - topicId: topicId, // Tambahkan ini - topicTitle: topicTitle, // Tambahkan ini + topicId: topicId, + topicTitle: topicTitle, ), ), ); diff --git a/lib/features/learning/modules/result/widgets/stay_result_widget.dart b/lib/features/learning/modules/result/widgets/stay_result_widget.dart new file mode 100644 index 0000000..23c9ede --- /dev/null +++ b/lib/features/learning/modules/result/widgets/stay_result_widget.dart @@ -0,0 +1,87 @@ +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 StayResultWidget extends StatelessWidget { + final String? nextLevel; + final int? score; + final String stdLearningId; + final String topicId; + final String topicTitle; + + const StayResultWidget({ + super.key, + required this.nextLevel, + required this.score, + required this.stdLearningId, + required this.topicId, + required this.topicTitle, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Your Result', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 25, + fontWeight: FontWeight.w900, + ), + ), + Text( + '$score/100', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 20, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + const SizedBox(height: 24), + SvgPicture.asset( + 'lib/features/learning/modules/result/assets/images/result_stay_illustration.svg', + width: 259, + ), + const SizedBox(height: 24), + Text( + 'Learning is a journey, let\'s explore this level further to deepen your knowledge.', + style: AppTextStyles.tetriaryTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + '$nextLevel', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 20, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Continue', + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => FeedbackScreen( + stdLearningId: stdLearningId, + topicId: topicId, // Tambahkan ini + topicTitle: topicTitle, // Tambahkan ini + ), + ), + ); + }, + ) + ], + ); + } +} diff --git a/lib/features/learning/screens/learning_screen.dart b/lib/features/learning/screens/learning_screen.dart index 275e9bc..2de11e7 100644 --- a/lib/features/learning/screens/learning_screen.dart +++ b/lib/features/learning/screens/learning_screen.dart @@ -35,8 +35,7 @@ class _LearningScreenState extends State Provider.of(context, listen: false); try { - // Reset data sebelum fetch - sectionProvider.resetData(); // Tambahkan method ini di SectionProvider + sectionProvider.resetData(); final token = await userProvider.getValidToken(); if (token != null) { @@ -61,26 +60,6 @@ class _LearningScreenState extends State } } - // Future _refreshSections() async { - // final userProvider = Provider.of(context, listen: false); - // final sectionProvider = - // Provider.of(context, listen: false); - - // try { - // final token = await userProvider.getValidToken(); - // if (token != null) { - // await sectionProvider.fetchSections(token); - // } - // } catch (e) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar( - // content: Text('Failed to refresh sections: $e'), - // backgroundColor: Colors.red, - // ), - // ); - // } - // } - Widget _buildErrorWidget(String error) { return Container( padding: const EdgeInsets.all(16), @@ -151,7 +130,6 @@ class _LearningScreenState extends State Expanded( child: Consumer( builder: (context, sectionProvider, _) { - // Prioritaskan loading state if (_isInitialLoading || sectionProvider.isLoading) { return ListView.builder( padding: const EdgeInsets.only(top: 8), @@ -161,12 +139,10 @@ class _LearningScreenState extends State ); } - // Tampilkan error jika ada if (sectionProvider.error != null) { return _buildErrorWidget(sectionProvider.error!); } - // Tampilkan sections atau pesan jika kosong if (sectionProvider.sections.isEmpty) { return RefreshIndicator( onRefresh: _initializeSections, @@ -202,7 +178,6 @@ class _LearningScreenState extends State ); } - // Tampilkan daftar sections return RefreshIndicator( onRefresh: _initializeSections, child: ListView.builder(