From b50ee898fe137c935a4443968c153b2e7f18be9b Mon Sep 17 00:00:00 2001 From: Naresh Pratista <2141720057@student.polinema.ac.id> Date: Sat, 23 Nov 2024 17:28:23 +0700 Subject: [PATCH] fix(refactor): description in topic list screen handling, and add various loading handling widget --- lib/core/services/constants.dart | 2 +- .../loading/shimmer_loading_widget.dart | 37 +++ lib/features/home/screens/home_screen.dart | 29 +-- .../home/widgets/progress_card_loading.dart | 26 ++ .../topics/screens/topics_list_screen.dart | 240 +++++++++++++++--- .../modules/topics/widgets/topic_card.dart | 7 +- .../topics/widgets/topic_card_loading.dart | 59 +++++ .../learning/screens/learning_screen.dart | 59 +---- .../learning/widgets/section_card.dart | 14 +- .../widgets/section_card_loading.dart | 59 +++++ .../widgets/section_card_shimmer.dart | 20 -- 11 files changed, 404 insertions(+), 148 deletions(-) create mode 100644 lib/core/widgets/loading/shimmer_loading_widget.dart create mode 100644 lib/features/home/widgets/progress_card_loading.dart create mode 100644 lib/features/learning/modules/topics/widgets/topic_card_loading.dart create mode 100644 lib/features/learning/widgets/section_card_loading.dart delete mode 100644 lib/features/learning/widgets/section_card_shimmer.dart diff --git a/lib/core/services/constants.dart b/lib/core/services/constants.dart index 5b0d470..d4dd96d 100644 --- a/lib/core/services/constants.dart +++ b/lib/core/services/constants.dart @@ -1 +1 @@ -const String baseUrl = 'https://7333-114-6-25-184.ngrok-free.app/'; +const String baseUrl = 'http://54.173.167.62/'; diff --git a/lib/core/widgets/loading/shimmer_loading_widget.dart b/lib/core/widgets/loading/shimmer_loading_widget.dart new file mode 100644 index 0000000..dd5ed01 --- /dev/null +++ b/lib/core/widgets/loading/shimmer_loading_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerLoadingWidget extends StatelessWidget { + final double width; + final double height; + final BorderRadius? borderRadius; + final EdgeInsets? padding; + + const ShimmerLoadingWidget({ + super.key, + required this.width, + required this.height, + this.borderRadius, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? EdgeInsets.zero, + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + period: const Duration(milliseconds: 1500), + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(8), + ), + ), + ), + ); + } +} diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index b9971f5..5704d20 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -7,6 +7,7 @@ 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/progress_card_loading.dart'; import 'package:english_learning/features/home/widgets/welcome_card.dart'; import 'package:english_learning/features/learning/screens/learning_screen.dart'; import 'package:english_learning/features/settings/modules/edit_profile/screens/edit_profile_screen.dart'; @@ -17,7 +18,6 @@ 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}); @@ -203,31 +203,6 @@ class _HomeContentState extends State { ); } - 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), - ), - ), - )), - ), - ), - ); - } - Widget _buildCompletedTopicsContent(CompletedTopicsProvider provider) { return ListView.builder( shrinkWrap: true, @@ -438,7 +413,7 @@ class _HomeContentState extends State { ), ), completedTopicsProvider.isLoading - ? _buildShimmerEffect() + ? const ProgressCardLoading() : completedTopicsProvider.completedTopics.isEmpty ? _buildNoDataWidget() : _buildCompletedTopicsContent( diff --git a/lib/features/home/widgets/progress_card_loading.dart b/lib/features/home/widgets/progress_card_loading.dart new file mode 100644 index 0000000..333b137 --- /dev/null +++ b/lib/features/home/widgets/progress_card_loading.dart @@ -0,0 +1,26 @@ +import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart'; +import 'package:flutter/material.dart'; + +class ProgressCardLoading extends StatelessWidget { + const ProgressCardLoading({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ShimmerLoadingWidget( + width: double.infinity, + height: 160, + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/topics/screens/topics_list_screen.dart b/lib/features/learning/modules/topics/screens/topics_list_screen.dart index b07e116..285c3c8 100644 --- a/lib/features/learning/modules/topics/screens/topics_list_screen.dart +++ b/lib/features/learning/modules/topics/screens/topics_list_screen.dart @@ -4,6 +4,7 @@ import 'package:english_learning/features/learning/modules/level/screens/level_l import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; import 'package:english_learning/features/learning/modules/topics/widgets/topic_card.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/topics/widgets/topic_card_loading.dart'; import 'package:english_learning/features/learning/provider/section_provider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -53,6 +54,102 @@ class _TopicsListScreenState extends State { } } + // Tambahkan method baru di dalam _TopicsListScreenState + void _showSectionDescriptionDialog(BuildContext context, String description) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: AppColors.whiteColor, + shape: BoxShape.rectangle, + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 10.0, + offset: Offset(0.0, 10.0), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 16), + decoration: const BoxDecoration( + color: AppColors.blueColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Center( + child: Text( + 'Section Description', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + + // Content + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Text( + description, + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 13, + fontWeight: FontWeight.w300, + height: 1.5, + ), + textAlign: TextAlign.justify, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 20), + ), + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white), + label: const Text( + 'Close', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final sectionProvider = Provider.of(context); @@ -71,6 +168,7 @@ class _TopicsListScreenState extends State { fontSize: 14, fontWeight: FontWeight.w900, ), + textAlign: TextAlign.center, ), flexibleSpace: Container( decoration: BoxDecoration( @@ -87,14 +185,13 @@ class _TopicsListScreenState extends State { _getFullImageUrl(selectedSection.thumbnail ?? ''), fit: BoxFit.cover, width: double.infinity, - height: 115, + height: 140, errorBuilder: (context, error, stackTrace) { - print('Error loading image: $error'); return Container( width: double.infinity, - height: 115, + height: 140, color: Colors.grey[300], - child: Icon( + child: const Icon( Icons.image_not_supported, color: Colors.grey, ), @@ -112,58 +209,123 @@ class _TopicsListScreenState extends State { ), Container( width: double.infinity, - height: 115, - color: AppColors.blackColor.withOpacity(0.5), + height: 140, + color: AppColors.blackColor.withOpacity(0.7), ), Center( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 42.0), - child: Text( - selectedSection.description, - style: AppTextStyles.whiteTextStyle.copyWith( - fontSize: 13, - fontWeight: FontWeight.w500, + padding: const EdgeInsets.symmetric(vertical: 32.0), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + selectedSection.description, + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () => _showSectionDescriptionDialog( + context, selectedSection.description), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Read More', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white.withOpacity(0.8), + decoration: TextDecoration.underline, + decorationColor: Colors.white.withOpacity(0.8), + ), + ), + ), + ), + ], ), - textAlign: TextAlign.center, ), ), ), ], ), - const SizedBox(height: 24), + // const SizedBox(height: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Consumer( builder: (context, topicProvider, _) { - if (topicProvider.topics.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } else if (topicProvider.error != null) { - return const Center(child: Text('No topics available')); - } else { + if (topicProvider.isLoading) { return ListView.builder( - itemCount: topicProvider.topics.length, - itemBuilder: (context, index) { - final topic = topicProvider.topics[index]; - return TopicCard( - title: topic.name, - description: topic.description, - isCompleted: topic.isCompleted, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LevelListScreen( - topicId: topic.id, - topicTitle: topic.name, - ), - ), - ); - }, - ); - }, + padding: const EdgeInsets.only(top: 8), + itemCount: 6, + itemBuilder: (context, index) => const TopicCardLoading(), ); } + + if (topicProvider.error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Failed to load topics', + style: AppTextStyles.greyTextStyle, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _fetchTopics, + child: const Text('Retry'), + ), + ], + ), + ); + } + + if (topicProvider.topics.isEmpty) { + return Center( + child: Text( + 'No topics available', + style: AppTextStyles.greyTextStyle, + ), + ); + } + + return ListView.builder( + itemCount: topicProvider.topics.length, + itemBuilder: (context, index) { + final topic = topicProvider.topics[index]; + return TopicCard( + title: topic.name, + description: topic.description, + isCompleted: topic.isCompleted, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LevelListScreen( + topicId: topic.id, + topicTitle: topic.name, + ), + ), + ); + }, + ); + }, + ); }, ), ), diff --git a/lib/features/learning/modules/topics/widgets/topic_card.dart b/lib/features/learning/modules/topics/widgets/topic_card.dart index afd6bfd..61854e9 100644 --- a/lib/features/learning/modules/topics/widgets/topic_card.dart +++ b/lib/features/learning/modules/topics/widgets/topic_card.dart @@ -6,12 +6,14 @@ class TopicCard extends StatelessWidget { final String description; final bool isCompleted; final VoidCallback? onTap; + final bool isLoading; const TopicCard({ super.key, this.onTap, required this.title, required this.description, required this.isCompleted, + this.isLoading = false, }); @override @@ -52,7 +54,7 @@ class TopicCard extends StatelessWidget { fontSize: 12, fontWeight: FontWeight.w500, ), - maxLines: 4, + maxLines: 3, overflow: TextOverflow.ellipsis, ), ], @@ -63,8 +65,7 @@ class TopicCard extends StatelessWidget { isCompleted ? Icons.check_circle : Icons.radio_button_unchecked, - color: - isCompleted ? AppColors.blueColor : AppColors.greyColor, + color: AppColors.blueColor, ), ], ), diff --git a/lib/features/learning/modules/topics/widgets/topic_card_loading.dart b/lib/features/learning/modules/topics/widgets/topic_card_loading.dart new file mode 100644 index 0000000..6c4ef54 --- /dev/null +++ b/lib/features/learning/modules/topics/widgets/topic_card_loading.dart @@ -0,0 +1,59 @@ +import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart'; +import 'package:flutter/material.dart'; + +class TopicCardLoading extends StatelessWidget { + const TopicCardLoading({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title shimmer + ShimmerLoadingWidget( + width: double.infinity, + height: 16, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 12), + // First line of description + ShimmerLoadingWidget( + width: double.infinity, + height: 12, + borderRadius: BorderRadius.circular(4), + padding: const EdgeInsets.only(bottom: 8), + ), + // Second line of description (shorter) + ShimmerLoadingWidget( + width: MediaQuery.of(context).size.width * 0.6, + height: 12, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + const SizedBox(width: 16), + // Completion status icon shimmer + ShimmerLoadingWidget( + width: 24, + height: 24, + borderRadius: BorderRadius.circular(12), // Make it circular + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/learning/screens/learning_screen.dart b/lib/features/learning/screens/learning_screen.dart index fb643bc..f9e2c67 100644 --- a/lib/features/learning/screens/learning_screen.dart +++ b/lib/features/learning/screens/learning_screen.dart @@ -2,7 +2,7 @@ import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/learning/provider/section_provider.dart'; import 'package:english_learning/features/learning/widgets/section_card.dart'; import 'package:english_learning/features/learning/modules/topics/screens/topics_list_screen.dart'; -import 'package:english_learning/features/learning/widgets/section_card_shimmer.dart'; +import 'package:english_learning/features/learning/widgets/section_card_loading.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:provider/provider.dart'; @@ -67,7 +67,12 @@ class _LearningScreenState extends State { child: Consumer( builder: (context, sectionProvider, _) { if (sectionProvider.isLoading) { - return _buildShimmerLoading(); + return ListView.builder( + padding: const EdgeInsets.only(top: 8), + itemCount: 6, + itemBuilder: (context, index) => + const SectionCardLoading(), + ); } else if (sectionProvider.error != null) { return Center( child: Column( @@ -97,7 +102,7 @@ class _LearningScreenState extends State { itemCount: sectionProvider.sections.length, itemBuilder: (context, index) { final section = sectionProvider.sections[index]; - return LearningCard( + return SectionCard( section: section, onTap: () => Navigator.push( context, @@ -120,52 +125,4 @@ class _LearningScreenState extends State { ), ); } - - Widget _buildShimmerLoading() { - return ListView.builder( - itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items - itemBuilder: (context, index) { - return ShimmerWidget( - child: Container( - margin: const EdgeInsets.only(bottom: 16), - child: Row( - children: [ - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 20, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), - ), - ), - const SizedBox(height: 8), - Container( - height: 20, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ), - ), - ], - ), - ), - ); - }, - ); - } } diff --git a/lib/features/learning/widgets/section_card.dart b/lib/features/learning/widgets/section_card.dart index 683f049..1fb7d9f 100644 --- a/lib/features/learning/widgets/section_card.dart +++ b/lib/features/learning/widgets/section_card.dart @@ -1,23 +1,24 @@ import 'package:english_learning/core/services/constants.dart'; +import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart'; import 'package:english_learning/features/learning/modules/model/section_model.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; -class LearningCard extends StatefulWidget { +class SectionCard extends StatefulWidget { final Section section; final VoidCallback? onTap; - const LearningCard({ + const SectionCard({ super.key, required this.section, this.onTap, }); @override - State createState() => _LearningCardState(); + State createState() => _SectionCardState(); } -class _LearningCardState extends State +class _SectionCardState extends State with SingleTickerProviderStateMixin { String _getFullImageUrl(String thumbnail) { if (thumbnail.startsWith('http')) { @@ -64,11 +65,10 @@ class _LearningCardState extends State }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; - return Container( + return ShimmerLoadingWidget( width: 90, height: 104, - color: Colors.grey[300], - child: Center(child: CircularProgressIndicator()), + borderRadius: BorderRadius.circular(8), ); }, )), diff --git a/lib/features/learning/widgets/section_card_loading.dart b/lib/features/learning/widgets/section_card_loading.dart new file mode 100644 index 0000000..6ec2837 --- /dev/null +++ b/lib/features/learning/widgets/section_card_loading.dart @@ -0,0 +1,59 @@ +import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart'; +import 'package:flutter/material.dart'; + +class SectionCardLoading extends StatelessWidget { + const SectionCardLoading({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 1, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShimmerLoadingWidget( + width: 90, + height: 104, + borderRadius: BorderRadius.circular(8), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title shimmer + ShimmerLoadingWidget( + width: 200, + height: 16, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 12), + //description + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < 3; i++) ...[ + const ShimmerLoadingWidget( + width: double.infinity, + height: 12, + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + const SizedBox(height: 6), + ], + ], + ), + ], + ), + ), + ], + ), + )); + } +} diff --git a/lib/features/learning/widgets/section_card_shimmer.dart b/lib/features/learning/widgets/section_card_shimmer.dart deleted file mode 100644 index 70c81f6..0000000 --- a/lib/features/learning/widgets/section_card_shimmer.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; - -class ShimmerWidget extends StatelessWidget { - final Widget child; - - const ShimmerWidget({ - super.key, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: child, - ); - } -}