fix: refactoring loading handling in various feature and improve performance
This commit is contained in:
parent
e8980afc60
commit
88ceaf7163
|
|
@ -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/';
|
||||
|
|
|
|||
|
|
@ -10,17 +10,36 @@ class CompletedTopicsRepository {
|
|||
Future<List<CompletedTopic>> getCompletedTopics(String token) async {
|
||||
try {
|
||||
final response = await _dioClient.getCompletedTopics(token);
|
||||
|
||||
// Tambahkan pengecekan status code dan payload
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> 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 {
|
||||
throw Exception(
|
||||
'Failed to load completed topics: ${response.statusMessage}');
|
||||
return []; // Kembalikan list kosong jika payload bukan list
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CustomFieldWidget> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_controller = widget.controller ?? TextEditingController();
|
||||
|
||||
context
|
||||
.read<ValidatorProvider>()
|
||||
.setController(widget.fieldName, _controller);
|
||||
|
|
@ -61,7 +64,9 @@ class _CustomFieldWidgetState extends State<CustomFieldWidget> {
|
|||
@override
|
||||
void dispose() {
|
||||
context.read<ValidatorProvider>().removeController(widget.fieldName);
|
||||
if (widget.controller == null) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +105,7 @@ class _CustomFieldWidgetState extends State<CustomFieldWidget> {
|
|||
obscureText: widget.obscureText,
|
||||
focusNode: widget.focusNode,
|
||||
onChanged: widget.onChanged,
|
||||
onFieldSubmitted: widget.onFieldSubmitted,
|
||||
enabled: widget.isEnabled,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
|
|
|
|||
|
|
@ -11,36 +11,134 @@ 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<SigninScreen> createState() => _SigninScreenState();
|
||||
}
|
||||
|
||||
class _SigninScreenState extends State<SigninScreen> {
|
||||
final TextEditingController _emailController = 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
|
||||
Widget build(BuildContext context) {
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
Provider.of<ValidatorProvider>(context, listen: false);
|
||||
|
||||
return Scaffold(
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.whiteColor,
|
||||
body: Center(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Consumer<ValidatorProvider>(
|
||||
builder: (context, validatorProvider, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
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: 4),
|
||||
Text(
|
||||
'Login to continue your personalized learning journey.',
|
||||
|
|
@ -50,27 +148,34 @@ class SigninScreen extends StatelessWidget {
|
|||
),
|
||||
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) {
|
||||
onChanged: (value) =>
|
||||
validatorProvider.validateField(
|
||||
'email',
|
||||
value,
|
||||
value?.trim(),
|
||||
validator: validatorProvider.emailValidator,
|
||||
);
|
||||
),
|
||||
onFieldSubmitted: (_) {
|
||||
FocusScope.of(context)
|
||||
.requestFocus(_passwordFocus);
|
||||
},
|
||||
errorText: validatorProvider.getError('email'),
|
||||
),
|
||||
|
|
@ -78,41 +183,49 @@ class SigninScreen extends StatelessWidget {
|
|||
CustomFieldWidget(
|
||||
fieldName: 'password',
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
isRequired: true,
|
||||
textInputAction: TextInputAction.next,
|
||||
textInputAction: TextInputAction.done,
|
||||
labelText: 'Password',
|
||||
hintText: 'Create a Strong Password',
|
||||
obscureText: validatorProvider.isObscure('password'),
|
||||
hintText: 'Enter Your Password',
|
||||
obscureText:
|
||||
validatorProvider.isObscure('password'),
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
validator: validatorProvider.passwordValidator,
|
||||
onChanged: (value) {
|
||||
onChanged: (value) =>
|
||||
validatorProvider.validateField(
|
||||
'password',
|
||||
value,
|
||||
validator: validatorProvider.passwordValidator,
|
||||
);
|
||||
},
|
||||
),
|
||||
onFieldSubmitted: (_) => _handleLogin(context),
|
||||
onSuffixIconTap: () =>
|
||||
validatorProvider.toggleVisibility('password'),
|
||||
errorText: validatorProvider.getError('password'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ForgotPasswordScreen()),
|
||||
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),
|
||||
Rect.fromLTWH(
|
||||
0, 0, bounds.width, bounds.height),
|
||||
),
|
||||
child: const Text(
|
||||
'Forgot Password?',
|
||||
|
|
@ -124,73 +237,20 @@ class SigninScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
Consumer<UserProvider>(
|
||||
builder: (context, userProvider, _) {
|
||||
return GlobalButton(
|
||||
text: 'Login',
|
||||
isLoading: userProvider.isLoading,
|
||||
onPressed: userProvider.isLoading
|
||||
? null
|
||||
: () async {
|
||||
// Validate email and password fields
|
||||
validatorProvider.validateField(
|
||||
'email',
|
||||
_emailController.text,
|
||||
validator: validatorProvider.emailValidator,
|
||||
: () => _handleLogin(context),
|
||||
);
|
||||
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),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -199,15 +259,33 @@ class SigninScreen extends StatelessWidget {
|
|||
style: AppTextStyles.blackTextStyle
|
||||
.copyWith(fontSize: 14),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<ValidatorProvider>().resetFields();
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context
|
||||
.read<ValidatorProvider>()
|
||||
.resetFields();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SignupScreen()),
|
||||
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(
|
||||
|
|
@ -218,6 +296,7 @@ class SigninScreen extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
@ -225,6 +304,9 @@ class SigninScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,22 +19,53 @@ class HistoryScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HistoryScreenState extends State<HistoryScreen> {
|
||||
bool _isInitialLoading = true;
|
||||
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Memuat data saat HistoryScreen diinisialisasi
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeHistory();
|
||||
}
|
||||
|
||||
Future<void> _initializeHistory() async {
|
||||
final historyProvider =
|
||||
Provider.of<HistoryProvider>(context, listen: false);
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
|
||||
|
||||
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
|
||||
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<HistoryScreen> {
|
|||
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,38 +131,47 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
}
|
||||
|
||||
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<UserProvider>(context, listen: false);
|
||||
await historyProvider.fetchLearningHistory(userProvider.jwtToken!);
|
||||
},
|
||||
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 Column(
|
||||
return AnimatedOpacity(
|
||||
opacity: 1.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Column(
|
||||
children: [
|
||||
ExerciseHistoryCard(
|
||||
exercise: historyProvider.learningHistory[index],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
// 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 = [];
|
||||
|
|
|
|||
|
|
@ -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,14 +158,76 @@ class _HomeContentState extends State<HomeContent> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Memanggil fetchCompletedTopics saat HomeContent diinisialisasi
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!);
|
||||
});
|
||||
|
||||
// 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() {
|
||||
|
|
@ -203,33 +265,110 @@ 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) {
|
||||
return ListView.builder(
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ListView.builder(
|
||||
key: ValueKey<int>(provider.completedTopics.length),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
|
||||
context,
|
||||
authProvider,
|
||||
completedTopicsProvider,
|
||||
child,
|
||||
) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _initializeData,
|
||||
child: Consumer2<UserProvider, CompletedTopicsProvider>(
|
||||
builder: (context, authProvider, completedTopicsProvider, child) {
|
||||
final userName = authProvider.getUserName() ?? 'Guest';
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -258,7 +397,8 @@ class _HomeContentState extends State<HomeContent> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
|
|
@ -269,8 +409,8 @@ class _HomeContentState extends State<HomeContent> {
|
|||
const SizedBox(width: 4.34),
|
||||
Text(
|
||||
'SEALS',
|
||||
style:
|
||||
AppTextStyles.logoTextStyle.copyWith(
|
||||
style: AppTextStyles.logoTextStyle
|
||||
.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
|
|
@ -306,8 +446,8 @@ class _HomeContentState extends State<HomeContent> {
|
|||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: userName,
|
||||
style:
|
||||
AppTextStyles.yellowTextStyle.copyWith(
|
||||
style: AppTextStyles.yellowTextStyle
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
|
|
@ -412,12 +552,12 @@ class _HomeContentState extends State<HomeContent> {
|
|||
],
|
||||
),
|
||||
),
|
||||
completedTopicsProvider.isLoading
|
||||
? const ProgressCardLoading()
|
||||
: completedTopicsProvider.completedTopics.isEmpty
|
||||
? _buildNoDataWidget()
|
||||
: _buildCompletedTopicsContent(
|
||||
completedTopicsProvider),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: completedTopicsProvider.isLoading
|
||||
? _buildLoadingWidget()
|
||||
: _buildProgressCard(completedTopicsProvider),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -425,6 +565,7 @@ class _HomeContentState extends State<HomeContent> {
|
|||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TopicsListScreen> {
|
|||
},
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<LearningScreen> createState() => _LearningScreenState();
|
||||
}
|
||||
|
||||
class _LearningScreenState extends State<LearningScreen> {
|
||||
class _LearningScreenState extends State<LearningScreen>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
bool _isInitialLoading = true;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_fetchSections();
|
||||
});
|
||||
_initializeSections();
|
||||
}
|
||||
|
||||
Future<void> _fetchSections() async {
|
||||
Future<void> _initializeSections() async {
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
final token = await userProvider.getValidToken();
|
||||
final sectionProvider =
|
||||
Provider.of<SectionProvider>(context, listen: false);
|
||||
|
||||
// Cek apakah sections sudah ada
|
||||
if (sectionProvider.sections.isEmpty) {
|
||||
try {
|
||||
final token = await userProvider.getValidToken();
|
||||
if (token != null) {
|
||||
await Provider.of<SectionProvider>(context, listen: false)
|
||||
.fetchSections(token);
|
||||
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<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
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
body: SafeArea(
|
||||
|
|
@ -66,15 +103,23 @@ class _LearningScreenState extends State<LearningScreen> {
|
|||
Expanded(
|
||||
child: Consumer<SectionProvider>(
|
||||
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(
|
||||
}
|
||||
|
||||
// Tampilkan error jika ada
|
||||
if (sectionProvider.error != null) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshSections,
|
||||
child: ListView(
|
||||
children: [
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -82,23 +127,59 @@ class _LearningScreenState extends State<LearningScreen> {
|
|||
'Error: ${sectionProvider.error}',
|
||||
style: AppTextStyles.greyTextStyle,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
// Tambahkan tombol retry jika diperlukan
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _fetchSections,
|
||||
child: Text('Retry'),
|
||||
onPressed: _refreshSections,
|
||||
child: const Text('Retry'),
|
||||
)
|
||||
],
|
||||
));
|
||||
} else if (sectionProvider.sections.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'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,
|
||||
itemBuilder: (context, index) {
|
||||
final section = sectionProvider.sections[index];
|
||||
|
|
@ -114,8 +195,8 @@ class _LearningScreenState extends State<LearningScreen> {
|
|||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<SectionCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
String _getFullImageUrl(String thumbnail) {
|
||||
if (thumbnail.startsWith('http')) {
|
||||
return thumbnail;
|
||||
|
|
@ -28,9 +31,73 @@ class _SectionCardState extends State<SectionCard>
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
super.build(context);
|
||||
|
||||
if (widget.isLoading) {
|
||||
return Card(
|
||||
color: AppColors.whiteColor,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -46,39 +113,53 @@ class _SectionCardState extends State<SectionCard>
|
|||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
_getFullImageUrl(widget.section.thumbnail ?? ''),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl:
|
||||
_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(
|
||||
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,
|
||||
widget.section?.name ?? '',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
|
|
@ -86,7 +167,7 @@ class _SectionCardState extends State<SectionCard>
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.section.description,
|
||||
widget.section?.description ?? '',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
@ -101,6 +182,9 @@ class _SectionCardState extends State<SectionCard>
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// section_card_loading.dart
|
||||
import 'package:english_learning/core/widgets/loading/shimmer_loading_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -28,32 +29,32 @@ class SectionCardLoading extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title shimmer
|
||||
ShimmerLoadingWidget(
|
||||
width: 200,
|
||||
height: 16,
|
||||
width: MediaQuery.of(context).size.width * 0.4,
|
||||
height: 20,
|
||||
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,
|
||||
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.all(Radius.circular(4)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
} else {
|
||||
// Jika sudah pernah install tetapi belum login, arahkan ke SigninScreen
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => SigninScreen()),
|
||||
MaterialPageRoute(builder: (_) => const SigninScreen()),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class WelcomeScreen extends StatelessWidget {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SigninScreen()),
|
||||
builder: (context) => const SigninScreen()),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user