Merge branch 'topiclist-fix-overflow' into 'master'

fix(refactor): description in topic list screen handling, and add various loading handling widget

See merge request profile-image/kedaireka/polinema-adapative-learning/mobile-adaptive-learning!16
This commit is contained in:
Naresh Pratista 2024-11-23 10:31:36 +00:00
commit e8980afc60
11 changed files with 404 additions and 148 deletions

View File

@ -1 +1 @@
const String baseUrl = 'https://7333-114-6-25-184.ngrok-free.app/'; const String baseUrl = 'http://54.173.167.62/';

View File

@ -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),
),
),
),
);
}
}

View File

@ -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/data/card_data.dart';
import 'package:english_learning/features/home/provider/completed_topics_provider.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.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/home/widgets/welcome_card.dart';
import 'package:english_learning/features/learning/screens/learning_screen.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'; 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:flutter_svg/flutter_svg.dart';
import 'package:google_nav_bar/google_nav_bar.dart'; import 'package:google_nav_bar/google_nav_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -203,31 +203,6 @@ class _HomeContentState extends State<HomeContent> {
); );
} }
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) { Widget _buildCompletedTopicsContent(CompletedTopicsProvider provider) {
return ListView.builder( return ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@ -438,7 +413,7 @@ class _HomeContentState extends State<HomeContent> {
), ),
), ),
completedTopicsProvider.isLoading completedTopicsProvider.isLoading
? _buildShimmerEffect() ? const ProgressCardLoading()
: completedTopicsProvider.completedTopics.isEmpty : completedTopicsProvider.completedTopics.isEmpty
? _buildNoDataWidget() ? _buildNoDataWidget()
: _buildCompletedTopicsContent( : _buildCompletedTopicsContent(

View File

@ -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),
),
),
);
}
}

View File

@ -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/providers/topic_provider.dart';
import 'package:english_learning/features/learning/modules/topics/widgets/topic_card.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/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:english_learning/features/learning/provider/section_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -53,6 +54,102 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
} }
} }
// 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sectionProvider = Provider.of<SectionProvider>(context); final sectionProvider = Provider.of<SectionProvider>(context);
@ -71,6 +168,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
), ),
textAlign: TextAlign.center,
), ),
flexibleSpace: Container( flexibleSpace: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -87,14 +185,13 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
_getFullImageUrl(selectedSection.thumbnail ?? ''), _getFullImageUrl(selectedSection.thumbnail ?? ''),
fit: BoxFit.cover, fit: BoxFit.cover,
width: double.infinity, width: double.infinity,
height: 115, height: 140,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return Container( return Container(
width: double.infinity, width: double.infinity,
height: 115, height: 140,
color: Colors.grey[300], color: Colors.grey[300],
child: Icon( child: const Icon(
Icons.image_not_supported, Icons.image_not_supported,
color: Colors.grey, color: Colors.grey,
), ),
@ -112,12 +209,18 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
), ),
Container( Container(
width: double.infinity, width: double.infinity,
height: 115, height: 140,
color: AppColors.blackColor.withOpacity(0.5), color: AppColors.blackColor.withOpacity(0.7),
), ),
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 42.0), 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( child: Text(
selectedSection.description, selectedSection.description,
style: AppTextStyles.whiteTextStyle.copyWith( style: AppTextStyles.whiteTextStyle.copyWith(
@ -125,22 +228,82 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
textAlign: TextAlign.center, 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),
),
), ),
), ),
), ),
], ],
), ),
const SizedBox(height: 24), ),
),
),
],
),
// const SizedBox(height: 12),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Consumer<TopicProvider>( child: Consumer<TopicProvider>(
builder: (context, topicProvider, _) { builder: (context, topicProvider, _) {
if (topicProvider.isLoading) {
return ListView.builder(
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) { if (topicProvider.topics.isEmpty) {
return const Center(child: CircularProgressIndicator()); return Center(
} else if (topicProvider.error != null) { child: Text(
return const Center(child: Text('No topics available')); 'No topics available',
} else { style: AppTextStyles.greyTextStyle,
),
);
}
return ListView.builder( return ListView.builder(
itemCount: topicProvider.topics.length, itemCount: topicProvider.topics.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -163,7 +326,6 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
); );
}, },
); );
}
}, },
), ),
), ),

View File

@ -6,12 +6,14 @@ class TopicCard extends StatelessWidget {
final String description; final String description;
final bool isCompleted; final bool isCompleted;
final VoidCallback? onTap; final VoidCallback? onTap;
final bool isLoading;
const TopicCard({ const TopicCard({
super.key, super.key,
this.onTap, this.onTap,
required this.title, required this.title,
required this.description, required this.description,
required this.isCompleted, required this.isCompleted,
this.isLoading = false,
}); });
@override @override
@ -52,7 +54,7 @@ class TopicCard extends StatelessWidget {
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
maxLines: 4, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
@ -63,8 +65,7 @@ class TopicCard extends StatelessWidget {
isCompleted isCompleted
? Icons.check_circle ? Icons.check_circle
: Icons.radio_button_unchecked, : Icons.radio_button_unchecked,
color: color: AppColors.blueColor,
isCompleted ? AppColors.blueColor : AppColors.greyColor,
), ),
], ],
), ),

View File

@ -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
),
],
),
),
);
}
}

View File

@ -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/provider/section_provider.dart';
import 'package:english_learning/features/learning/widgets/section_card.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/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:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -67,7 +67,12 @@ class _LearningScreenState extends State<LearningScreen> {
child: Consumer<SectionProvider>( child: Consumer<SectionProvider>(
builder: (context, sectionProvider, _) { builder: (context, sectionProvider, _) {
if (sectionProvider.isLoading) { 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) { } else if (sectionProvider.error != null) {
return Center( return Center(
child: Column( child: Column(
@ -97,7 +102,7 @@ class _LearningScreenState extends State<LearningScreen> {
itemCount: sectionProvider.sections.length, itemCount: sectionProvider.sections.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final section = sectionProvider.sections[index]; final section = sectionProvider.sections[index];
return LearningCard( return SectionCard(
section: section, section: section,
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
@ -120,52 +125,4 @@ class _LearningScreenState extends State<LearningScreen> {
), ),
); );
} }
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),
),
),
],
),
),
],
),
),
);
},
);
}
} }

View File

@ -1,23 +1,24 @@
import 'package:english_learning/core/services/constants.dart'; 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:english_learning/features/learning/modules/model/section_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
class LearningCard extends StatefulWidget { class SectionCard extends StatefulWidget {
final Section section; final Section section;
final VoidCallback? onTap; final VoidCallback? onTap;
const LearningCard({ const SectionCard({
super.key, super.key,
required this.section, required this.section,
this.onTap, this.onTap,
}); });
@override @override
State<LearningCard> createState() => _LearningCardState(); State<SectionCard> createState() => _SectionCardState();
} }
class _LearningCardState extends State<LearningCard> class _SectionCardState extends State<SectionCard>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
String _getFullImageUrl(String thumbnail) { String _getFullImageUrl(String thumbnail) {
if (thumbnail.startsWith('http')) { if (thumbnail.startsWith('http')) {
@ -64,11 +65,10 @@ class _LearningCardState extends State<LearningCard>
}, },
loadingBuilder: (context, child, loadingProgress) { loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Container( return ShimmerLoadingWidget(
width: 90, width: 90,
height: 104, height: 104,
color: Colors.grey[300], borderRadius: BorderRadius.circular(8),
child: Center(child: CircularProgressIndicator()),
); );
}, },
)), )),

View File

@ -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),
],
],
),
],
),
),
],
),
));
}
}

View File

@ -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,
);
}
}