diff --git a/lib/core/services/constants.dart b/lib/core/services/constants.dart index d4dd96d..73323e3 100644 --- a/lib/core/services/constants.dart +++ b/lib/core/services/constants.dart @@ -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/'; diff --git a/lib/core/services/repositories/completed_topics_repository.dart b/lib/core/services/repositories/completed_topics_repository.dart index ad489df..a8e0b0e 100644 --- a/lib/core/services/repositories/completed_topics_repository.dart +++ b/lib/core/services/repositories/completed_topics_repository.dart @@ -10,17 +10,36 @@ class CompletedTopicsRepository { Future> getCompletedTopics(String token) async { try { final response = await _dioClient.getCompletedTopics(token); + + // Tambahkan pengecekan status code dan payload if (response.statusCode == 200) { - final List topicsData = response.data['payload']; - return topicsData.map((data) => CompletedTopic.fromJson(data)).toList(); + // Cek apakah payload null atau bukan list + 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 { - throw Exception( - 'Failed to load completed topics: ${response.statusMessage}'); + // Tangani status code selain 200 + return []; } } 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) { - throw Exception('Unexpected error: $e'); + // Log error tidak terduga + print('Unexpected error: $e'); + return []; // Kembalikan list kosong untuk error lainnya } } } diff --git a/lib/core/widgets/form_field/custom_field_widget.dart b/lib/core/widgets/form_field/custom_field_widget.dart index 0aa04de..196bc9c 100644 --- a/lib/core/widgets/form_field/custom_field_widget.dart +++ b/lib/core/widgets/form_field/custom_field_widget.dart @@ -21,6 +21,7 @@ class CustomFieldWidget extends StatefulWidget { final Function()? onSuffixIconTap; final bool isRequired; final bool isEnabled; + final Function(String?)? onFieldSubmitted; const CustomFieldWidget({ super.key, @@ -40,6 +41,7 @@ class CustomFieldWidget extends StatefulWidget { this.onSuffixIconTap, this.isRequired = false, this.isEnabled = true, + this.onFieldSubmitted, }); @override @@ -52,7 +54,8 @@ class _CustomFieldWidgetState extends State { @override void initState() { super.initState(); - _controller = TextEditingController(); + _controller = widget.controller ?? TextEditingController(); + context .read() .setController(widget.fieldName, _controller); @@ -61,7 +64,9 @@ class _CustomFieldWidgetState extends State { @override void dispose() { context.read().removeController(widget.fieldName); - _controller.dispose(); + if (widget.controller == null) { + _controller.dispose(); + } super.dispose(); } @@ -100,6 +105,7 @@ class _CustomFieldWidgetState extends State { obscureText: widget.obscureText, focusNode: widget.focusNode, onChanged: widget.onChanged, + onFieldSubmitted: widget.onFieldSubmitted, enabled: widget.isEnabled, decoration: InputDecoration( hintText: widget.hintText, diff --git a/lib/features/auth/screens/signin/signin_screen.dart b/lib/features/auth/screens/signin/signin_screen.dart index 73ee33a..fce3178 100644 --- a/lib/features/auth/screens/signin/signin_screen.dart +++ b/lib/features/auth/screens/signin/signin_screen.dart @@ -11,216 +11,298 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; -class SigninScreen extends StatelessWidget { +class SigninScreen extends StatefulWidget { + const SigninScreen({super.key}); + + @override + State createState() => _SigninScreenState(); +} + +class _SigninScreenState extends State { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); + final FocusNode _emailFocus = FocusNode(); + final FocusNode _passwordFocus = FocusNode(); + final _formKey = GlobalKey(); - SigninScreen({super.key}); + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _emailFocus.dispose(); + _passwordFocus.dispose(); + super.dispose(); + } + + Future _handleLogin(BuildContext context) async { + if (!_formKey.currentState!.validate()) return; + + final userProvider = context.read(); + final validatorProvider = context.read(); + + 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 Widget build(BuildContext context) { - final userProvider = Provider.of(context, listen: false); - Provider.of(context, listen: false); - - return Scaffold( - backgroundColor: AppColors.whiteColor, - body: Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: SingleChildScrollView( - child: Consumer( - builder: (context, validatorProvider, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - Text( - 'Welcome Back!', - style: AppTextStyles.blueTextStyle.copyWith( - fontSize: 30, - fontWeight: FontWeight.w900, - ), - ), - const SizedBox(height: 4), - Text( - 'Login to continue your personalized learning journey.', - style: AppTextStyles.greyTextStyle.copyWith( - 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, + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + backgroundColor: AppColors.whiteColor, + body: SafeArea( + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Consumer( + builder: (context, validatorProvider, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Hero( + tag: 'welcome_text', + child: Material( + color: Colors.transparent, + child: Text( + 'Welcome Back!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 30, + fontWeight: FontWeight.w900, + ), ), ), ), - ), - ], - ), - const SizedBox(height: 24), - 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 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().resetFields(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SignupScreen()), - ); - }, - child: Text( - 'Sign Up Here', - style: AppTextStyles.blueTextStyle.copyWith( + const SizedBox(height: 4), + Text( + 'Login to continue your personalized learning journey.', + style: AppTextStyles.greyTextStyle.copyWith( 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( + 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() + .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), + ], + ); + }, + ), + ), + ), ), ), ), diff --git a/lib/features/history/screens/history_screen.dart b/lib/features/history/screens/history_screen.dart index a9c4fa8..c2f229f 100644 --- a/lib/features/history/screens/history_screen.dart +++ b/lib/features/history/screens/history_screen.dart @@ -19,22 +19,53 @@ class HistoryScreen extends StatefulWidget { } class _HistoryScreenState extends State { + bool _isInitialLoading = true; + + bool get wantKeepAlive => true; + @override void initState() { super.initState(); - // Memuat data saat HistoryScreen diinisialisasi - WidgetsBinding.instance.addPostFrameCallback((_) { - final historyProvider = - Provider.of(context, listen: false); - final userProvider = Provider.of(context, listen: false); - historyProvider.fetchLearningHistory(userProvider.jwtToken!); - }); + _initializeHistory(); + } + + Future _initializeHistory() async { + final historyProvider = + Provider.of(context, listen: false); + final userProvider = Provider.of(context, listen: false); + + try { + await historyProvider.fetchLearningHistory(userProvider.jwtToken!); + } catch (e) { + print('Error initializing history: $e'); + } finally { + setState(() { + _isInitialLoading = false; + }); + } + } + + Future _refreshHistory() async { + final historyProvider = + Provider.of(context, listen: false); + final userProvider = Provider.of(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 Widget _buildShimmerLoading() { return ListView.builder( - itemCount: 5, // Jumlah item shimmer + itemCount: 5, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), @@ -43,10 +74,17 @@ class _HistoryScreenState extends State { highlightColor: Colors.grey[100]!, child: Container( width: double.infinity, - height: 100, // Sesuaikan dengan tinggi card history + height: 100, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + Colors.grey[200]!, + Colors.grey[100]!, + Colors.grey[200]!, + ], + ), ), ), ), @@ -93,37 +131,46 @@ class _HistoryScreenState extends State { } Widget _buildContent(BuildContext context, HistoryProvider historyProvider) { - if (historyProvider.isLoading) { + // Prioritaskan initial loading + if (_isInitialLoading || historyProvider.isLoading) { return _buildShimmerLoading(); } + // Tangani error if (historyProvider.error != null) { return isNotFoundError(historyProvider.error!) ? _buildEmptyState(context) : _buildErrorState(context, historyProvider.error!); } + // Tampilkan empty state jika tidak ada history if (historyProvider.learningHistory.isEmpty) { return _buildEmptyState(context); } + // Tampilkan daftar history dengan refresh indicator return RefreshIndicator( - onRefresh: () async { - final userProvider = Provider.of(context, listen: false); - await historyProvider.fetchLearningHistory(userProvider.jwtToken!); - }, - child: ListView.builder( - itemCount: historyProvider.learningHistory.length, - itemBuilder: (context, index) { - return Column( - children: [ - ExerciseHistoryCard( - exercise: historyProvider.learningHistory[index], + onRefresh: _refreshHistory, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: ListView.builder( + key: ValueKey(historyProvider.learningHistory.length), + itemCount: historyProvider.learningHistory.length, + itemBuilder: (context, index) { + return AnimatedOpacity( + opacity: 1.0, + duration: const Duration(milliseconds: 500), + child: Column( + children: [ + ExerciseHistoryCard( + exercise: historyProvider.learningHistory[index], + ), + const SizedBox(height: 8.0), + ], ), - const SizedBox(height: 8.0), - ], - ); - }, + ); + }, + ), ), ); } diff --git a/lib/features/home/provider/completed_topics_provider.dart b/lib/features/home/provider/completed_topics_provider.dart index f1d4943..fb2d6da 100644 --- a/lib/features/home/provider/completed_topics_provider.dart +++ b/lib/features/home/provider/completed_topics_provider.dart @@ -1,5 +1,7 @@ // 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/features/home/models/completed_topics_model.dart'; import 'package:flutter/foundation.dart'; @@ -30,8 +32,16 @@ class CompletedTopicsProvider with ChangeNotifier { try { 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) { // Tangani error _completedTopics = []; diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 5704d20..ba6a7d4 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -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/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'; @@ -18,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_nav_bar/google_nav_bar.dart'; import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -158,16 +158,78 @@ class _HomeContentState extends State { @override void initState() { super.initState(); - // Memanggil fetchCompletedTopics saat HomeContent diinisialisasi WidgetsBinding.instance.addPostFrameCallback((_) { - final userProvider = Provider.of(context, listen: false); - final completedTopicsProvider = - Provider.of(context, listen: false); - completedTopicsProvider.resetData(); - completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!); + _initializeData(); }); } + Future _initializeData() async { + final userProvider = Provider.of(context, listen: false); + final completedTopicsProvider = + Provider.of(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() { return Padding( padding: const EdgeInsets.all(12.0), @@ -203,228 +265,307 @@ class _HomeContentState extends State { ); } + 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) { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric( - horizontal: 16.0, + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: ListView.builder( + key: ValueKey(provider.completedTopics.length), + 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 Widget build(BuildContext context) { - return Consumer2(builder: ( - context, - authProvider, - completedTopicsProvider, - child, - ) { - final userName = authProvider.getUserName() ?? 'Guest'; + return RefreshIndicator( + onRefresh: _initializeData, + child: Consumer2( + builder: (context, authProvider, completedTopicsProvider, child) { + final userName = authProvider.getUserName() ?? 'Guest'; - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: AppColors.gradientTheme, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), ), - ), - child: Padding( - padding: const EdgeInsets.only( - top: 60.0, - left: 18.34, - right: 16.0, - bottom: 34.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - 'lib/features/home/assets/images/Logo.svg', - width: 31, - ), - const SizedBox(width: 4.34), - Text( - 'SEALS', - style: - AppTextStyles.logoTextStyle.copyWith( - fontSize: 28, - fontWeight: FontWeight.w700, + child: Padding( + padding: const EdgeInsets.only( + top: 60.0, + left: 18.34, + right: 16.0, + bottom: 34.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + 'lib/features/home/assets/images/Logo.svg', + width: 31, ), - ), - ], - ), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const EditProfileScreen(), + const SizedBox(width: 4.34), + Text( + 'SEALS', + style: AppTextStyles.logoTextStyle + .copyWith( + fontSize: 28, + fontWeight: FontWeight.w700, + ), ), - ); - }, - child: const Icon( - BootstrapIcons.person_circle, - size: 28, - color: AppColors.whiteColor, + ], ), - ), - ], - ), - const SizedBox(height: 17), - RichText( - text: TextSpan( - text: 'Hi, ', - style: AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, - ), - children: [ - TextSpan( - text: userName, - style: - AppTextStyles.yellowTextStyle.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, - ), - ), - TextSpan( - text: '!', - style: - AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const EditProfileScreen(), + ), + ); + }, + child: const Icon( + BootstrapIcons.person_circle, + size: 28, + color: AppColors.whiteColor, ), ), ], ), + const SizedBox(height: 17), + RichText( + text: TextSpan( + text: 'Hi, ', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + children: [ + TextSpan( + text: userName, + style: AppTextStyles.yellowTextStyle + .copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + TextSpan( + text: '!', + style: + AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Text( + 'Let\'s evolve together', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + CarouselSlider.builder( + itemCount: cardData.cardData.length, + itemBuilder: (context, index, realIndex) { + return WelcomeCard(cardModel: cardData.cardData[index]); + }, + options: CarouselOptions( + height: 168, + viewportFraction: 0.9, + enlargeCenterPage: true, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 3), + autoPlayAnimationDuration: const Duration(milliseconds: 800), + autoPlayCurve: Curves.fastOutSlowIn, + onPageChanged: (index, reason) { + setState( + () { + _currentPage = index; + }, + ); + }, + ), + ), + const SizedBox(height: 16), + SliderWidget( + currentPage: _currentPage, + itemCount: cardData.cardData.length, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: 24.0, + right: 24.0, + bottom: 47.0, + ), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + const Icon( + BootstrapIcons.info_circle, + color: AppColors.tetriaryColor, + size: 16, ), - const SizedBox(height: 8), + const SizedBox(width: 8), Text( - 'Let\'s evolve together', - style: AppTextStyles.whiteTextStyle.copyWith( + 'Your Last Journey.', + style: AppTextStyles.tetriaryTextStyle.copyWith( fontSize: 12, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w800, ), ), ], ), ), - ), - ], - ), - ], - ), - 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, - ), - ), - ], + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: completedTopicsProvider.isLoading + ? _buildLoadingWidget() + : _buildProgressCard(completedTopicsProvider), ), - ), - completedTopicsProvider.isLoading - ? const ProgressCardLoading() - : completedTopicsProvider.completedTopics.isEmpty - ? _buildNoDataWidget() - : _buildCompletedTopicsContent( - completedTopicsProvider), - ], + ], + ), ), ), - ), - ], - ), - ); - }); + ], + ), + ); + }), + ); } } 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 285c3c8..ae0916c 100644 --- a/lib/features/learning/modules/topics/screens/topics_list_screen.dart +++ b/lib/features/learning/modules/topics/screens/topics_list_screen.dart @@ -1,4 +1,5 @@ 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/learning/modules/level/screens/level_list_screen.dart'; import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; @@ -199,11 +200,9 @@ class _TopicsListScreenState extends State { }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; - return Container( - width: 90, - height: 104, - color: Colors.grey[300], - child: const Center(child: CircularProgressIndicator()), + return const ShimmerLoadingWidget( + height: 140, + width: double.infinity, ); }, ), diff --git a/lib/features/learning/screens/learning_screen.dart b/lib/features/learning/screens/learning_screen.dart index f9e2c67..b3abd58 100644 --- a/lib/features/learning/screens/learning_screen.dart +++ b/lib/features/learning/screens/learning_screen.dart @@ -8,37 +8,74 @@ import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:provider/provider.dart'; class LearningScreen extends StatefulWidget { - const LearningScreen({ - super.key, - }); + const LearningScreen({super.key}); @override State createState() => _LearningScreenState(); } -class _LearningScreenState extends State { +class _LearningScreenState extends State + with AutomaticKeepAliveClientMixin { + bool _isInitialLoading = true; + + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _fetchSections(); - }); + _initializeSections(); } - Future _fetchSections() async { + Future _initializeSections() async { final userProvider = Provider.of(context, listen: false); - final token = await userProvider.getValidToken(); + final sectionProvider = + Provider.of(context, listen: false); - if (token != null) { - await Provider.of(context, listen: false) - .fetchSections(token); + // Cek apakah sections sudah ada + if (sectionProvider.sections.isEmpty) { + 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 { - print('No valid token found. User might need to log in.'); + setState(() { + _isInitialLoading = false; + }); + } + } + + Future _refreshSections() async { + final userProvider = Provider.of(context, listen: false); + final sectionProvider = + Provider.of(context, listen: false); + + try { + final token = await userProvider.getValidToken(); + if (token != null) { + await sectionProvider.fetchSections(token); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to refresh sections: $e'), + backgroundColor: Colors.red, + ), + ); } } @override Widget build(BuildContext context) { + super.build(context); return Scaffold( backgroundColor: AppColors.bgSoftColor, body: SafeArea( @@ -66,39 +103,83 @@ class _LearningScreenState extends State { Expanded( child: Consumer( builder: (context, sectionProvider, _) { - if (sectionProvider.isLoading) { + // Prioritaskan loading state + if (_isInitialLoading || sectionProvider.isLoading) { return ListView.builder( padding: const EdgeInsets.only(top: 8), - itemCount: 6, + itemCount: 4, itemBuilder: (context, index) => const SectionCardLoading(), ); - } else if (sectionProvider.error != null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Error: ${sectionProvider.error}', - style: AppTextStyles.greyTextStyle, - ), - SizedBox(height: 16), - // Tambahkan tombol retry jika diperlukan - ElevatedButton( - onPressed: _fetchSections, - child: Text('Retry'), - ) - ], - )); - } else if (sectionProvider.sections.isEmpty) { - return Center( - child: Text( - 'No sections available', - style: AppTextStyles.greyTextStyle, + } + + // Tampilkan error jika ada + if (sectionProvider.error != null) { + return RefreshIndicator( + onRefresh: _refreshSections, + child: ListView( + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Error: ${sectionProvider.error}', + style: AppTextStyles.greyTextStyle, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _refreshSections, + child: const Text('Retry'), + ) + ], + ), + ), + ], ), ); - } 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, itemBuilder: (context, index) { final section = sectionProvider.sections[index]; @@ -114,8 +195,8 @@ class _LearningScreenState extends State { ), ); }, - ); - } + ), + ); }, ), ), diff --git a/lib/features/learning/widgets/section_card.dart b/lib/features/learning/widgets/section_card.dart index 1fb7d9f..769f288 100644 --- a/lib/features/learning/widgets/section_card.dart +++ b/lib/features/learning/widgets/section_card.dart @@ -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:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:cached_network_image/cached_network_image.dart'; class SectionCard extends StatefulWidget { - final Section section; + final Section? section; final VoidCallback? onTap; + final bool isLoading; const SectionCard({ super.key, - required this.section, + this.section, this.onTap, + this.isLoading = false, }); @override @@ -19,7 +22,7 @@ class SectionCard extends StatefulWidget { } class _SectionCardState extends State - with SingleTickerProviderStateMixin { + with AutomaticKeepAliveClientMixin { String _getFullImageUrl(String thumbnail) { if (thumbnail.startsWith('http')) { return thumbnail; @@ -28,71 +31,56 @@ class _SectionCardState extends State } } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { - return GestureDetector( - onTap: widget.onTap, - child: Card( + super.build(context); + + if (widget.isLoading) { + return Card( color: AppColors.whiteColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 1, - margin: const EdgeInsets.symmetric(vertical: 6.0), + margin: const EdgeInsets.symmetric(vertical: 8.0), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - _getFullImageUrl(widget.section.thumbnail ?? ''), - 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), - ); - }, - )), + ShimmerLoadingWidget( + width: 90, + height: 104, + borderRadius: BorderRadius.circular(8), + ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.section.name, - style: AppTextStyles.blackTextStyle.copyWith( - fontSize: 16, - fontWeight: FontWeight.w900, - ), + ShimmerLoadingWidget( + width: MediaQuery.of(context).size.width * 0.4, + height: 20, + borderRadius: BorderRadius.circular(4), ), - const SizedBox(height: 4), - Text( - widget.section.description, - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 13, - fontWeight: FontWeight.w500, + 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), + ), + ), ), - maxLines: 4, - overflow: TextOverflow.ellipsis, ), ], ), @@ -100,6 +88,102 @@ class _SectionCardState extends State ], ), ), + ); + } + + 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, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), ), ); } diff --git a/lib/features/learning/widgets/section_card_loading.dart b/lib/features/learning/widgets/section_card_loading.dart index 6ec2837..4e7d7fe 100644 --- a/lib/features/learning/widgets/section_card_loading.dart +++ b/lib/features/learning/widgets/section_card_loading.dart @@ -1,3 +1,4 @@ +// section_card_loading.dart import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart'; import 'package:flutter/material.dart'; @@ -7,53 +8,53 @@ class SectionCardLoading extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + 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: [ + 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), - ], - ], - ), - ], - ), - ), - ], - ), - )); + ), + ); } } diff --git a/lib/features/splash/screens/splash_screen.dart b/lib/features/splash/screens/splash_screen.dart index 8b9e2ff..71703a7 100644 --- a/lib/features/splash/screens/splash_screen.dart +++ b/lib/features/splash/screens/splash_screen.dart @@ -53,7 +53,7 @@ class _SplashScreenState extends State { } else { // Jika sudah pernah install tetapi belum login, arahkan ke SigninScreen Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => SigninScreen()), + MaterialPageRoute(builder: (_) => const SigninScreen()), ); } }); diff --git a/lib/features/welcome/screens/welcome_screen.dart b/lib/features/welcome/screens/welcome_screen.dart index 329dd00..1b1a30f 100644 --- a/lib/features/welcome/screens/welcome_screen.dart +++ b/lib/features/welcome/screens/welcome_screen.dart @@ -83,7 +83,7 @@ class WelcomeScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => SigninScreen()), + builder: (context) => const SigninScreen()), ); }, child: Text(