fix: refactoring loading handling in various feature and improve performance

This commit is contained in:
Naresh Pratista 2024-11-26 06:19:28 +07:00
parent e8980afc60
commit 88ceaf7163
13 changed files with 1052 additions and 581 deletions

View File

@ -1 +1,2 @@
const String baseUrl = 'http://54.173.167.62/'; const String baseUrl =
'https://e901-2001-448a-50a0-656c-cd9-ff82-635b-5b6e.ngrok-free.app/';

View File

@ -10,17 +10,36 @@ class CompletedTopicsRepository {
Future<List<CompletedTopic>> getCompletedTopics(String token) async { Future<List<CompletedTopic>> getCompletedTopics(String token) async {
try { try {
final response = await _dioClient.getCompletedTopics(token); final response = await _dioClient.getCompletedTopics(token);
// Tambahkan pengecekan status code dan payload
if (response.statusCode == 200) { if (response.statusCode == 200) {
final List<dynamic> topicsData = response.data['payload']; // Cek apakah payload null atau bukan list
return topicsData.map((data) => CompletedTopic.fromJson(data)).toList(); if (response.data['payload'] == null) {
return []; // Kembalikan list kosong jika payload null
}
// Pastikan payload adalah list
final dynamic payloadData = response.data['payload'];
if (payloadData is List) {
return payloadData
.map((data) => CompletedTopic.fromJson(data))
.toList();
} else {
return []; // Kembalikan list kosong jika payload bukan list
}
} else { } else {
throw Exception( // Tangani status code selain 200
'Failed to load completed topics: ${response.statusMessage}'); return [];
} }
} on DioException catch (e) { } on DioException catch (e) {
throw Exception('Network error: ${e.message}'); // Log error jika perlu
print('Network error: ${e.message}');
return []; // Kembalikan list kosong untuk error jaringan
} catch (e) { } catch (e) {
throw Exception('Unexpected error: $e'); // Log error tidak terduga
print('Unexpected error: $e');
return []; // Kembalikan list kosong untuk error lainnya
} }
} }
} }

View File

@ -21,6 +21,7 @@ class CustomFieldWidget extends StatefulWidget {
final Function()? onSuffixIconTap; final Function()? onSuffixIconTap;
final bool isRequired; final bool isRequired;
final bool isEnabled; final bool isEnabled;
final Function(String?)? onFieldSubmitted;
const CustomFieldWidget({ const CustomFieldWidget({
super.key, super.key,
@ -40,6 +41,7 @@ class CustomFieldWidget extends StatefulWidget {
this.onSuffixIconTap, this.onSuffixIconTap,
this.isRequired = false, this.isRequired = false,
this.isEnabled = true, this.isEnabled = true,
this.onFieldSubmitted,
}); });
@override @override
@ -52,7 +54,8 @@ class _CustomFieldWidgetState extends State<CustomFieldWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = TextEditingController(); _controller = widget.controller ?? TextEditingController();
context context
.read<ValidatorProvider>() .read<ValidatorProvider>()
.setController(widget.fieldName, _controller); .setController(widget.fieldName, _controller);
@ -61,7 +64,9 @@ class _CustomFieldWidgetState extends State<CustomFieldWidget> {
@override @override
void dispose() { void dispose() {
context.read<ValidatorProvider>().removeController(widget.fieldName); context.read<ValidatorProvider>().removeController(widget.fieldName);
_controller.dispose(); if (widget.controller == null) {
_controller.dispose();
}
super.dispose(); super.dispose();
} }
@ -100,6 +105,7 @@ class _CustomFieldWidgetState extends State<CustomFieldWidget> {
obscureText: widget.obscureText, obscureText: widget.obscureText,
focusNode: widget.focusNode, focusNode: widget.focusNode,
onChanged: widget.onChanged, onChanged: widget.onChanged,
onFieldSubmitted: widget.onFieldSubmitted,
enabled: widget.isEnabled, enabled: widget.isEnabled,
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.hintText, hintText: widget.hintText,

View File

@ -11,216 +11,298 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class SigninScreen extends StatelessWidget { class SigninScreen extends StatefulWidget {
const SigninScreen({super.key});
@override
State<SigninScreen> createState() => _SigninScreenState();
}
class _SigninScreenState extends State<SigninScreen> {
final TextEditingController _emailController = TextEditingController(); final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
final FocusNode _emailFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();
final _formKey = GlobalKey<FormState>();
SigninScreen({super.key}); @override
void initState() {
super.initState();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_emailFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
Future<void> _handleLogin(BuildContext context) async {
if (!_formKey.currentState!.validate()) return;
final userProvider = context.read<UserProvider>();
final validatorProvider = context.read<ValidatorProvider>();
try {
final isSuccess = await userProvider.login(
email: _emailController.text.trim(),
password: _passwordController.text,
);
if (!mounted) return;
if (isSuccess) {
// Hide keyboard before navigation
FocusScope.of(context).unfocus();
await Navigator.pushAndRemoveUntil(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const HomeScreen(),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 300),
),
(route) => false,
);
// Reset form
validatorProvider.resetFields();
_emailController.clear();
_passwordController.clear();
} else {
_handleLoginError(context, userProvider.errorCode);
}
} catch (e) {
if (mounted) {
CustomSnackBar.show(
context,
message: 'An unexpected error occurred. Please try again.',
isError: true,
);
}
}
}
void _handleLoginError(BuildContext context, int? errorCode) {
String message = 'Login failed, please check your credentials';
switch (errorCode) {
case 403:
message = 'Please verify your email first to continue.';
break;
case 401:
message = 'Invalid email or password. Please try again.';
break;
case 429:
message = 'Too many attempts. Please try again later.';
break;
}
CustomSnackBar.show(context, message: message, isError: true);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userProvider = Provider.of<UserProvider>(context, listen: false); return GestureDetector(
Provider.of<ValidatorProvider>(context, listen: false); onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
return Scaffold( backgroundColor: AppColors.whiteColor,
backgroundColor: AppColors.whiteColor, body: SafeArea(
body: Center( child: Center(
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Consumer<ValidatorProvider>( child: Form(
builder: (context, validatorProvider, child) { key: _formKey,
return Column( child: Consumer<ValidatorProvider>(
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, validatorProvider, child) {
children: [ return Column(
const SizedBox(height: 32), crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'Welcome Back!', const SizedBox(height: 24),
style: AppTextStyles.blueTextStyle.copyWith( Hero(
fontSize: 30, tag: 'welcome_text',
fontWeight: FontWeight.w900, child: Material(
), color: Colors.transparent,
), child: Text(
const SizedBox(height: 4), 'Welcome Back!',
Text( style: AppTextStyles.blueTextStyle.copyWith(
'Login to continue your personalized learning journey.', fontSize: 30,
style: AppTextStyles.greyTextStyle.copyWith( fontWeight: FontWeight.w900,
fontSize: 14, ),
),
),
const SizedBox(height: 26),
Center(
child: SvgPicture.asset(
'lib/features/auth/assets/images/login_illustration.svg',
width: 200,
),
),
const SizedBox(height: 30),
CustomFieldWidget(
fieldName: 'email',
controller: _emailController,
isRequired: true,
textInputAction: TextInputAction.next,
labelText: 'Email Address',
hintText: 'Enter Your Email Address',
keyboardType: TextInputType.emailAddress,
validator: validatorProvider.emailValidator,
onChanged: (value) {
validatorProvider.validateField(
'email',
value,
validator: validatorProvider.emailValidator,
);
},
errorText: validatorProvider.getError('email'),
),
const SizedBox(height: 14),
CustomFieldWidget(
fieldName: 'password',
controller: _passwordController,
isRequired: true,
textInputAction: TextInputAction.next,
labelText: 'Password',
hintText: 'Create a Strong Password',
obscureText: validatorProvider.isObscure('password'),
keyboardType: TextInputType.visiblePassword,
validator: validatorProvider.passwordValidator,
onChanged: (value) {
validatorProvider.validateField(
'password',
value,
validator: validatorProvider.passwordValidator,
);
},
onSuffixIconTap: () =>
validatorProvider.toggleVisibility('password'),
errorText: validatorProvider.getError('password'),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ForgotPasswordScreen()),
);
},
child: ShaderMask(
shaderCallback: (bounds) =>
AppColors.gradientTheme.createShader(
Rect.fromLTWH(0, 0, bounds.width, bounds.height),
),
child: const Text(
'Forgot Password?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
), ),
), const SizedBox(height: 4),
], Text(
), 'Login to continue your personalized learning journey.',
const SizedBox(height: 24), style: AppTextStyles.greyTextStyle.copyWith(
GlobalButton(
text: 'Login',
isLoading: userProvider.isLoading,
onPressed: userProvider.isLoading
? null
: () async {
// Validate email and password fields
validatorProvider.validateField(
'email',
_emailController.text,
validator: validatorProvider.emailValidator,
);
validatorProvider.validateField(
'password',
_passwordController.text,
validator: validatorProvider.passwordValidator,
);
// If no errors, proceed with login
if (validatorProvider.getError('email') == null &&
validatorProvider.getError('password') ==
null) {
final isSuccess = await userProvider.login(
email: _emailController.text,
password: _passwordController.text,
);
if (isSuccess) {
// Navigate to HomeScreen after successful login
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) =>
const HomeScreen()),
(Route<dynamic> route) =>
false, // Remove all previous routes
).then((_) {
// Reset the fields after login
validatorProvider.resetFields();
_emailController.clear();
_passwordController.clear();
});
} else {
// Handle specific error for unverified user
if (userProvider.errorCode == 403) {
CustomSnackBar.show(
context,
message:
'User is not validated! Please verify your email first.',
isError: true,
);
} else {
CustomSnackBar.show(
context,
message:
'Login failed, please check your credentials',
isError: true,
);
}
}
}
},
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Haven\'t joined us yet? ',
style: AppTextStyles.blackTextStyle
.copyWith(fontSize: 14),
),
GestureDetector(
onTap: () {
context.read<ValidatorProvider>().resetFields();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignupScreen()),
);
},
child: Text(
'Sign Up Here',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold,
), ),
), ),
), const SizedBox(height: 26),
], Center(
), child: Hero(
], tag: 'login_illustration',
); child: SvgPicture.asset(
}, 'lib/features/auth/assets/images/login_illustration.svg',
width: 200,
),
),
),
const SizedBox(height: 30),
CustomFieldWidget(
fieldName: 'email',
controller: _emailController,
focusNode: _emailFocus,
isRequired: true,
textInputAction: TextInputAction.next,
labelText: 'Email Address',
hintText: 'Enter Your Email Address',
keyboardType: TextInputType.emailAddress,
validator: validatorProvider.emailValidator,
onChanged: (value) =>
validatorProvider.validateField(
'email',
value?.trim(),
validator: validatorProvider.emailValidator,
),
onFieldSubmitted: (_) {
FocusScope.of(context)
.requestFocus(_passwordFocus);
},
errorText: validatorProvider.getError('email'),
),
const SizedBox(height: 14),
CustomFieldWidget(
fieldName: 'password',
controller: _passwordController,
focusNode: _passwordFocus,
isRequired: true,
textInputAction: TextInputAction.done,
labelText: 'Password',
hintText: 'Enter Your Password',
obscureText:
validatorProvider.isObscure('password'),
keyboardType: TextInputType.visiblePassword,
validator: validatorProvider.passwordValidator,
onChanged: (value) =>
validatorProvider.validateField(
'password',
value,
validator: validatorProvider.passwordValidator,
),
onFieldSubmitted: (_) => _handleLogin(context),
onSuffixIconTap: () =>
validatorProvider.toggleVisibility('password'),
errorText: validatorProvider.getError('password'),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ForgotPasswordScreen(),
),
);
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: ShaderMask(
shaderCallback: (bounds) =>
AppColors.gradientTheme.createShader(
Rect.fromLTWH(
0, 0, bounds.width, bounds.height),
),
child: const Text(
'Forgot Password?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
const SizedBox(height: 24),
Consumer<UserProvider>(
builder: (context, userProvider, _) {
return GlobalButton(
text: 'Login',
isLoading: userProvider.isLoading,
onPressed: userProvider.isLoading
? null
: () => _handleLogin(context),
);
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Haven\'t joined us yet? ',
style: AppTextStyles.blackTextStyle
.copyWith(fontSize: 14),
),
TextButton(
onPressed: () {
context
.read<ValidatorProvider>()
.resetFields();
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation,
secondaryAnimation) =>
SignupScreen(),
transitionsBuilder: (context, animation,
secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'Sign Up Here',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
],
);
},
),
),
),
), ),
), ),
), ),

View File

@ -19,22 +19,53 @@ class HistoryScreen extends StatefulWidget {
} }
class _HistoryScreenState extends State<HistoryScreen> { class _HistoryScreenState extends State<HistoryScreen> {
bool _isInitialLoading = true;
bool get wantKeepAlive => true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Memuat data saat HistoryScreen diinisialisasi _initializeHistory();
WidgetsBinding.instance.addPostFrameCallback((_) { }
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false); Future<void> _initializeHistory() async {
final userProvider = Provider.of<UserProvider>(context, listen: false); final historyProvider =
historyProvider.fetchLearningHistory(userProvider.jwtToken!); Provider.of<HistoryProvider>(context, listen: false);
}); final userProvider = Provider.of<UserProvider>(context, listen: false);
try {
await historyProvider.fetchLearningHistory(userProvider.jwtToken!);
} catch (e) {
print('Error initializing history: $e');
} finally {
setState(() {
_isInitialLoading = false;
});
}
}
Future<void> _refreshHistory() async {
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context, listen: false);
try {
await historyProvider.fetchLearningHistory(userProvider.jwtToken!);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to refresh history: $e'),
backgroundColor: Colors.red,
),
);
}
} }
// Tambahkan method untuk shimmer loading // Tambahkan method untuk shimmer loading
Widget _buildShimmerLoading() { Widget _buildShimmerLoading() {
return ListView.builder( return ListView.builder(
itemCount: 5, // Jumlah item shimmer itemCount: 5,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
@ -43,10 +74,17 @@ class _HistoryScreenState extends State<HistoryScreen> {
highlightColor: Colors.grey[100]!, highlightColor: Colors.grey[100]!,
child: Container( child: Container(
width: double.infinity, width: double.infinity,
height: 100, // Sesuaikan dengan tinggi card history height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
Colors.grey[200]!,
Colors.grey[100]!,
Colors.grey[200]!,
],
),
), ),
), ),
), ),
@ -93,37 +131,46 @@ class _HistoryScreenState extends State<HistoryScreen> {
} }
Widget _buildContent(BuildContext context, HistoryProvider historyProvider) { Widget _buildContent(BuildContext context, HistoryProvider historyProvider) {
if (historyProvider.isLoading) { // Prioritaskan initial loading
if (_isInitialLoading || historyProvider.isLoading) {
return _buildShimmerLoading(); return _buildShimmerLoading();
} }
// Tangani error
if (historyProvider.error != null) { if (historyProvider.error != null) {
return isNotFoundError(historyProvider.error!) return isNotFoundError(historyProvider.error!)
? _buildEmptyState(context) ? _buildEmptyState(context)
: _buildErrorState(context, historyProvider.error!); : _buildErrorState(context, historyProvider.error!);
} }
// Tampilkan empty state jika tidak ada history
if (historyProvider.learningHistory.isEmpty) { if (historyProvider.learningHistory.isEmpty) {
return _buildEmptyState(context); return _buildEmptyState(context);
} }
// Tampilkan daftar history dengan refresh indicator
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: _refreshHistory,
final userProvider = Provider.of<UserProvider>(context, listen: false); child: AnimatedSwitcher(
await historyProvider.fetchLearningHistory(userProvider.jwtToken!); duration: const Duration(milliseconds: 300),
}, child: ListView.builder(
child: ListView.builder( key: ValueKey(historyProvider.learningHistory.length),
itemCount: historyProvider.learningHistory.length, itemCount: historyProvider.learningHistory.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Column( return AnimatedOpacity(
children: [ opacity: 1.0,
ExerciseHistoryCard( duration: const Duration(milliseconds: 500),
exercise: historyProvider.learningHistory[index], child: Column(
children: [
ExerciseHistoryCard(
exercise: historyProvider.learningHistory[index],
),
const SizedBox(height: 8.0),
],
), ),
const SizedBox(height: 8.0), );
], },
); ),
},
), ),
); );
} }

View File

@ -1,5 +1,7 @@
// lib/features/home/providers/completed_topics_provider.dart // lib/features/home/providers/completed_topics_provider.dart
// ignore_for_file: unnecessary_null_comparison
import 'package:english_learning/core/services/repositories/completed_topics_repository.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:english_learning/features/home/models/completed_topics_model.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -30,8 +32,16 @@ class CompletedTopicsProvider with ChangeNotifier {
try { try {
final result = await _repository.getCompletedTopics(token); final result = await _repository.getCompletedTopics(token);
_completedTopics = result;
_error = null; // Tambahkan pengecekan null secara eksplisit
if (result == null) {
_completedTopics = []; // Set ke list kosong
_error = 'No topics found'; // Beri pesan default
} else {
// Pastikan result adalah List
_completedTopics = result;
_error = null;
}
} catch (e) { } catch (e) {
// Tangani error // Tangani error
_completedTopics = []; _completedTopics = [];

View File

@ -7,7 +7,6 @@ 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';
@ -18,6 +17,7 @@ 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});
@ -158,16 +158,78 @@ class _HomeContentState extends State<HomeContent> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Memanggil fetchCompletedTopics saat HomeContent diinisialisasi
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final userProvider = Provider.of<UserProvider>(context, listen: false); _initializeData();
final completedTopicsProvider =
Provider.of<CompletedTopicsProvider>(context, listen: false);
completedTopicsProvider.resetData();
completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!);
}); });
} }
Future<void> _initializeData() async {
final userProvider = Provider.of<UserProvider>(context, listen: false);
final completedTopicsProvider =
Provider.of<CompletedTopicsProvider>(context, listen: false);
try {
// Reset data sebelum fetch
completedTopicsProvider.resetData();
// Fetch completed topics
await completedTopicsProvider
.fetchCompletedTopics(userProvider.jwtToken!);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to load data: ${e.toString()}'),
action: SnackBarAction(
label: 'Retry',
onPressed: _initializeData,
),
),
);
}
}
}
Widget _buildErrorWidget(String error) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Colors.red[300],
),
const SizedBox(height: 16),
Text(
'Oops! Something went wrong',
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
error,
textAlign: TextAlign.center,
style: AppTextStyles.disableTextStyle.copyWith(fontSize: 14),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initializeData,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
);
}
Widget _buildNoDataWidget() { Widget _buildNoDataWidget() {
return Padding( return Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
@ -203,228 +265,307 @@ class _HomeContentState extends State<HomeContent> {
); );
} }
Widget _buildProgressCard(CompletedTopicsProvider provider) {
if (provider.error != null) {
return _buildErrorWidget(provider.error!);
}
if (provider.completedTopics.isEmpty) {
return _buildNoDataWidget();
}
return _buildCompletedTopicsContent(provider);
}
Widget _buildCompletedTopicsContent(CompletedTopicsProvider provider) { Widget _buildCompletedTopicsContent(CompletedTopicsProvider provider) {
return ListView.builder( return AnimatedSwitcher(
shrinkWrap: true, duration: const Duration(milliseconds: 300),
physics: const NeverScrollableScrollPhysics(), child: ListView.builder(
padding: const EdgeInsets.symmetric( key: ValueKey<int>(provider.completedTopics.length),
horizontal: 16.0, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemCount: 1,
itemBuilder: (context, index) {
return ProgressCard(
completedTopic: provider.completedTopics,
);
},
),
);
}
Widget _buildLoadingWidget() {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
],
),
), ),
itemCount: 1,
itemBuilder: (context, index) {
return ProgressCard(
completedTopic: provider.completedTopics,
);
},
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: ( return RefreshIndicator(
context, onRefresh: _initializeData,
authProvider, child: Consumer2<UserProvider, CompletedTopicsProvider>(
completedTopicsProvider, builder: (context, authProvider, completedTopicsProvider, child) {
child, final userName = authProvider.getUserName() ?? 'Guest';
) {
final userName = authProvider.getUserName() ?? 'Guest';
return SingleChildScrollView( return SingleChildScrollView(
child: Column( physics: const AlwaysScrollableScrollPhysics(),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Stack( children: [
children: [ Stack(
Column( children: [
mainAxisSize: MainAxisSize.min, Column(
children: [ mainAxisSize: MainAxisSize.min,
Container( children: [
width: double.infinity, Container(
decoration: BoxDecoration( width: double.infinity,
gradient: AppColors.gradientTheme, decoration: BoxDecoration(
borderRadius: const BorderRadius.only( gradient: AppColors.gradientTheme,
bottomLeft: Radius.circular(24), borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(24), bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
), ),
), child: Padding(
child: Padding( padding: const EdgeInsets.only(
padding: const EdgeInsets.only( top: 60.0,
top: 60.0, left: 18.34,
left: 18.34, right: 16.0,
right: 16.0, bottom: 34.0,
bottom: 34.0, ),
), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( mainAxisAlignment:
mainAxisAlignment: MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
SvgPicture.asset( SvgPicture.asset(
'lib/features/home/assets/images/Logo.svg', 'lib/features/home/assets/images/Logo.svg',
width: 31, width: 31,
),
const SizedBox(width: 4.34),
Text(
'SEALS',
style:
AppTextStyles.logoTextStyle.copyWith(
fontSize: 28,
fontWeight: FontWeight.w700,
), ),
), const SizedBox(width: 4.34),
], Text(
), 'SEALS',
GestureDetector( style: AppTextStyles.logoTextStyle
onTap: () { .copyWith(
Navigator.push( fontSize: 28,
context, fontWeight: FontWeight.w700,
MaterialPageRoute( ),
builder: (context) =>
const EditProfileScreen(),
), ),
); ],
},
child: const Icon(
BootstrapIcons.person_circle,
size: 28,
color: AppColors.whiteColor,
), ),
), GestureDetector(
], onTap: () {
), Navigator.push(
const SizedBox(height: 17), context,
RichText( MaterialPageRoute(
text: TextSpan( builder: (context) =>
text: 'Hi, ', const EditProfileScreen(),
style: AppTextStyles.whiteTextStyle.copyWith( ),
fontWeight: FontWeight.w700, );
fontSize: 18, },
), child: const Icon(
children: <TextSpan>[ BootstrapIcons.person_circle,
TextSpan( size: 28,
text: userName, color: AppColors.whiteColor,
style:
AppTextStyles.yellowTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
TextSpan(
text: '!',
style:
AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
), ),
), ),
], ],
), ),
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( Text(
'Let\'s evolve together', 'Your Last Journey.',
style: AppTextStyles.whiteTextStyle.copyWith( style: AppTextStyles.tetriaryTextStyle.copyWith(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w800,
), ),
), ),
], ],
), ),
), ),
), AnimatedSwitcher(
], duration: const Duration(milliseconds: 300),
), child: completedTopicsProvider.isLoading
], ? _buildLoadingWidget()
), : _buildProgressCard(completedTopicsProvider),
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(width: 8),
Text(
'Your Last Journey.',
style: AppTextStyles.tetriaryTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
],
), ),
), ],
completedTopicsProvider.isLoading ),
? const ProgressCardLoading()
: completedTopicsProvider.completedTopics.isEmpty
? _buildNoDataWidget()
: _buildCompletedTopicsContent(
completedTopicsProvider),
],
), ),
), ),
), ],
], ),
), );
); }),
}); );
} }
} }

View File

@ -1,4 +1,5 @@
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/auth/provider/user_provider.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/learning/modules/level/screens/level_list_screen.dart'; import 'package:english_learning/features/learning/modules/level/screens/level_list_screen.dart';
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart';
@ -199,11 +200,9 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
}, },
loadingBuilder: (context, child, loadingProgress) { loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Container( return const ShimmerLoadingWidget(
width: 90, height: 140,
height: 104, width: double.infinity,
color: Colors.grey[300],
child: const Center(child: CircularProgressIndicator()),
); );
}, },
), ),

View File

@ -8,37 +8,74 @@ import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class LearningScreen extends StatefulWidget { class LearningScreen extends StatefulWidget {
const LearningScreen({ const LearningScreen({super.key});
super.key,
});
@override @override
State<LearningScreen> createState() => _LearningScreenState(); State<LearningScreen> createState() => _LearningScreenState();
} }
class _LearningScreenState extends State<LearningScreen> { class _LearningScreenState extends State<LearningScreen>
with AutomaticKeepAliveClientMixin {
bool _isInitialLoading = true;
@override
bool get wantKeepAlive => true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { _initializeSections();
_fetchSections();
});
} }
Future<void> _fetchSections() async { Future<void> _initializeSections() async {
final userProvider = Provider.of<UserProvider>(context, listen: false); final userProvider = Provider.of<UserProvider>(context, listen: false);
final token = await userProvider.getValidToken(); final sectionProvider =
Provider.of<SectionProvider>(context, listen: false);
if (token != null) { // Cek apakah sections sudah ada
await Provider.of<SectionProvider>(context, listen: false) if (sectionProvider.sections.isEmpty) {
.fetchSections(token); try {
final token = await userProvider.getValidToken();
if (token != null) {
await sectionProvider.fetchSections(token);
}
} catch (e) {
print('Error initializing sections: $e');
} finally {
setState(() {
_isInitialLoading = false;
});
}
} else { } else {
print('No valid token found. User might need to log in.'); setState(() {
_isInitialLoading = false;
});
}
}
Future<void> _refreshSections() async {
final userProvider = Provider.of<UserProvider>(context, listen: false);
final sectionProvider =
Provider.of<SectionProvider>(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,
),
);
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgSoftColor, backgroundColor: AppColors.bgSoftColor,
body: SafeArea( body: SafeArea(
@ -66,39 +103,83 @@ class _LearningScreenState extends State<LearningScreen> {
Expanded( Expanded(
child: Consumer<SectionProvider>( child: Consumer<SectionProvider>(
builder: (context, sectionProvider, _) { builder: (context, sectionProvider, _) {
if (sectionProvider.isLoading) { // Prioritaskan loading state
if (_isInitialLoading || sectionProvider.isLoading) {
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
itemCount: 6, itemCount: 4,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
const SectionCardLoading(), const SectionCardLoading(),
); );
} else if (sectionProvider.error != null) { }
return Center(
child: Column( // Tampilkan error jika ada
mainAxisAlignment: MainAxisAlignment.center, if (sectionProvider.error != null) {
children: [ return RefreshIndicator(
Text( onRefresh: _refreshSections,
'Error: ${sectionProvider.error}', child: ListView(
style: AppTextStyles.greyTextStyle, children: [
), Center(
SizedBox(height: 16), child: Column(
// Tambahkan tombol retry jika diperlukan mainAxisAlignment: MainAxisAlignment.center,
ElevatedButton( children: [
onPressed: _fetchSections, Text(
child: Text('Retry'), 'Error: ${sectionProvider.error}',
) style: AppTextStyles.greyTextStyle,
], ),
)); const SizedBox(height: 16),
} else if (sectionProvider.sections.isEmpty) { ElevatedButton(
return Center( onPressed: _refreshSections,
child: Text( child: const Text('Retry'),
'No sections available', )
style: AppTextStyles.greyTextStyle, ],
),
),
],
), ),
); );
} else { }
return ListView.builder(
// Tampilkan sections atau pesan jika kosong
if (sectionProvider.sections.isEmpty) {
return RefreshIndicator(
onRefresh: _refreshSections,
child: ListView(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.library_books_outlined,
size: 80,
color: Colors.grey,
),
const SizedBox(height: 16),
Text(
'No sections available',
style: AppTextStyles.greyTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _refreshSections,
child: const Text('Refresh'),
)
],
),
),
],
),
);
}
// Tampilkan daftar sections
return RefreshIndicator(
onRefresh: _refreshSections,
child: ListView.builder(
itemCount: sectionProvider.sections.length, itemCount: sectionProvider.sections.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final section = sectionProvider.sections[index]; final section = sectionProvider.sections[index];
@ -114,8 +195,8 @@ class _LearningScreenState extends State<LearningScreen> {
), ),
); );
}, },
); ),
} );
}, },
), ),
), ),

View File

@ -3,15 +3,18 @@ import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dar
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';
import 'package:cached_network_image/cached_network_image.dart';
class SectionCard extends StatefulWidget { class SectionCard extends StatefulWidget {
final Section section; final Section? section;
final VoidCallback? onTap; final VoidCallback? onTap;
final bool isLoading;
const SectionCard({ const SectionCard({
super.key, super.key,
required this.section, this.section,
this.onTap, this.onTap,
this.isLoading = false,
}); });
@override @override
@ -19,7 +22,7 @@ class SectionCard extends StatefulWidget {
} }
class _SectionCardState extends State<SectionCard> class _SectionCardState extends State<SectionCard>
with SingleTickerProviderStateMixin { with AutomaticKeepAliveClientMixin {
String _getFullImageUrl(String thumbnail) { String _getFullImageUrl(String thumbnail) {
if (thumbnail.startsWith('http')) { if (thumbnail.startsWith('http')) {
return thumbnail; return thumbnail;
@ -28,71 +31,56 @@ class _SectionCardState extends State<SectionCard>
} }
} }
@override
bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( super.build(context);
onTap: widget.onTap,
child: Card( if (widget.isLoading) {
return Card(
color: AppColors.whiteColor, color: AppColors.whiteColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
elevation: 1, elevation: 1,
margin: const EdgeInsets.symmetric(vertical: 6.0), margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( ShimmerLoadingWidget(
borderRadius: BorderRadius.circular(8), width: 90,
child: Image.network( height: 104,
_getFullImageUrl(widget.section.thumbnail ?? ''), borderRadius: BorderRadius.circular(8),
width: 90, ),
height: 104,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return Container(
width: 90,
height: 104,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return ShimmerLoadingWidget(
width: 90,
height: 104,
borderRadius: BorderRadius.circular(8),
);
},
)),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( ShimmerLoadingWidget(
widget.section.name, width: MediaQuery.of(context).size.width * 0.4,
style: AppTextStyles.blackTextStyle.copyWith( height: 20,
fontSize: 16, borderRadius: BorderRadius.circular(4),
fontWeight: FontWeight.w900,
),
), ),
const SizedBox(height: 4), const SizedBox(height: 12),
Text( Column(
widget.section.description, children: List.generate(
style: AppTextStyles.disableTextStyle.copyWith( 3,
fontSize: 13, (index) => Padding(
fontWeight: FontWeight.w500, padding:
EdgeInsets.only(bottom: index != 2 ? 6.0 : 0),
child: ShimmerLoadingWidget(
width: MediaQuery.of(context).size.width *
(0.8 - (index * 0.1)),
height: 12,
borderRadius: BorderRadius.circular(4),
),
),
), ),
maxLines: 4,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
@ -100,6 +88,102 @@ class _SectionCardState extends State<SectionCard>
], ],
), ),
), ),
);
}
return Hero(
tag: 'section_${widget.section?.id}',
child: Material(
type: MaterialType.transparency,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: GestureDetector(
onTap: widget.onTap,
child: Card(
color: AppColors.whiteColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 1,
margin: const EdgeInsets.symmetric(vertical: 6.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl:
_getFullImageUrl(widget.section?.thumbnail ?? ''),
width: 90,
height: 104,
fit: BoxFit.cover,
placeholder: (context, url) => ShimmerLoadingWidget(
width: 90,
height: 104,
borderRadius: BorderRadius.circular(8),
),
errorWidget: (context, url, error) => Container(
width: 90,
height: 104,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_not_supported,
color: Colors.grey[400],
size: 24,
),
const SizedBox(height: 4),
Text(
'Image not\navailable',
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.section?.name ?? '',
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
widget.section?.description ?? '',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 13,
fontWeight: FontWeight.w500,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
),
),
), ),
); );
} }

View File

@ -1,3 +1,4 @@
// section_card_loading.dart
import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart'; import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,53 +8,53 @@ class SectionCardLoading extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: Colors.white, color: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), 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: [
ShimmerLoadingWidget(
width: MediaQuery.of(context).size.width * 0.4,
height: 20,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 12),
Column(
children: List.generate(
3,
(index) => Padding(
padding: EdgeInsets.only(bottom: index != 2 ? 6.0 : 0),
child: ShimmerLoadingWidget(
width: MediaQuery.of(context).size.width *
(0.8 - (index * 0.1)),
height: 12,
borderRadius: BorderRadius.circular(4),
),
),
),
),
],
),
),
],
), ),
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

@ -53,7 +53,7 @@ class _SplashScreenState extends State<SplashScreen> {
} else { } else {
// Jika sudah pernah install tetapi belum login, arahkan ke SigninScreen // Jika sudah pernah install tetapi belum login, arahkan ke SigninScreen
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SigninScreen()), MaterialPageRoute(builder: (_) => const SigninScreen()),
); );
} }
}); });

View File

@ -83,7 +83,7 @@ class WelcomeScreen extends StatelessWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SigninScreen()), builder: (context) => const SigninScreen()),
); );
}, },
child: Text( child: Text(