feat: add shimmer loading skeletons for various pages and components

- Implemented UiShimmer as a core shimmer wrapper for animated gradient effects.
- Created shimmer presets for list items, stats cards, section headers, and more.
- Developed specific skeletons for billing, invoices, coverage, hubs, reports, payments, shifts, and home pages.
- Enhanced user experience by providing visual placeholders during data loading.
This commit is contained in:
Achintha Isuru
2026-03-10 13:21:30 -04:00
parent 3f112f5eb7
commit 0f0714c55b
36 changed files with 1594 additions and 36 deletions

View File

@@ -8,6 +8,7 @@ import 'package:core_localization/core_localization.dart';
import '../blocs/payments/payments_bloc.dart';
import '../blocs/payments/payments_event.dart';
import '../blocs/payments/payments_state.dart';
import '../widgets/payments_page_skeleton.dart';
import '../widgets/payment_stats_card.dart';
import '../widgets/payment_history_item.dart';
import '../widgets/earnings_graph.dart';
@@ -41,7 +42,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
},
builder: (BuildContext context, PaymentsState state) {
if (state is PaymentsLoading) {
return const Center(child: CircularProgressIndicator());
return const PaymentsPageSkeleton();
} else if (state is PaymentsError) {
return Center(

View File

@@ -0,0 +1,148 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer loading skeleton for the payments page.
///
/// Mimics the loaded layout: a gradient header with balance and period tabs,
/// an earnings graph placeholder, stat cards, and a recent payments list.
class PaymentsPageSkeleton extends StatelessWidget {
/// Creates a [PaymentsPageSkeleton].
const PaymentsPageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
child: Column(
children: [
// Header section with gradient
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
UiColors.primary,
UiColors.primary.withValues(alpha: 0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: EdgeInsets.fromLTRB(
UiConstants.space5,
MediaQuery.of(context).padding.top + UiConstants.space6,
UiConstants.space5,
UiConstants.space8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title placeholder
const UiShimmerLine(width: 120, height: 24),
const SizedBox(height: UiConstants.space6),
// Balance center
const Center(
child: Column(
children: [
UiShimmerLine(width: 100, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 160, height: 36),
],
),
),
const SizedBox(height: UiConstants.space4),
// Period tabs placeholder
UiShimmerBox(
width: double.infinity,
height: 40,
borderRadius: UiConstants.radiusMd,
),
],
),
),
// Main content offset upwards
Transform.translate(
offset: const Offset(0, -UiConstants.space4),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Earnings graph placeholder
UiShimmerBox(
width: double.infinity,
height: 180,
borderRadius: UiConstants.radiusLg,
),
const SizedBox(height: UiConstants.space6),
// Quick stats row
Row(
children: [
Expanded(child: UiShimmerStatsCard()),
const SizedBox(width: UiConstants.space3),
Expanded(child: UiShimmerStatsCard()),
],
),
const SizedBox(height: UiConstants.space8),
// Recent Payments header
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3),
// Payment history items
UiShimmerList(
itemCount: 4,
itemBuilder: (index) => const _PaymentItemSkeleton(),
),
],
),
),
),
],
),
),
);
}
}
/// Skeleton for a single payment history item.
///
/// Matches the [PaymentHistoryItem] layout with a leading icon, title/subtitle
/// lines, and trailing amount text.
class _PaymentItemSkeleton extends StatelessWidget {
const _PaymentItemSkeleton();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: const Row(
children: [
UiShimmerCircle(size: 40),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UiShimmerLine(width: 140, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 100, height: 12),
],
),
),
SizedBox(width: UiConstants.space3),
UiShimmerLine(width: 60, height: 16),
],
),
);
}
}