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:
@@ -8,6 +8,7 @@ import 'package:krow_core/core.dart';
|
||||
import '../blocs/billing_bloc.dart';
|
||||
import '../blocs/billing_event.dart';
|
||||
import '../blocs/billing_state.dart';
|
||||
import '../widgets/billing_page_skeleton.dart';
|
||||
import '../widgets/invoice_history_section.dart';
|
||||
import '../widgets/pending_invoices_section.dart';
|
||||
import '../widgets/spending_breakdown_card.dart';
|
||||
@@ -179,10 +180,7 @@ class _BillingViewState extends State<BillingView> {
|
||||
|
||||
Widget _buildContent(BuildContext context, BillingState state) {
|
||||
if (state.status == BillingStatus.loading) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(UiConstants.space10),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
return const BillingPageSkeleton();
|
||||
}
|
||||
|
||||
if (state.status == BillingStatus.failure) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../blocs/billing_bloc.dart';
|
||||
import '../blocs/billing_event.dart';
|
||||
import '../blocs/billing_state.dart';
|
||||
import '../models/billing_invoice_model.dart';
|
||||
import '../widgets/invoices_list_skeleton.dart';
|
||||
|
||||
class InvoiceReadyPage extends StatelessWidget {
|
||||
const InvoiceReadyPage({super.key});
|
||||
@@ -30,7 +31,7 @@ class InvoiceReadyView extends StatelessWidget {
|
||||
body: BlocBuilder<BillingBloc, BillingState>(
|
||||
builder: (BuildContext context, BillingState state) {
|
||||
if (state.status == BillingStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const InvoicesListSkeleton();
|
||||
}
|
||||
|
||||
if (state.invoiceHistory.isEmpty) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:krow_core/core.dart';
|
||||
|
||||
import '../blocs/billing_bloc.dart';
|
||||
import '../blocs/billing_state.dart';
|
||||
import '../widgets/invoices_list_skeleton.dart';
|
||||
import '../widgets/pending_invoices_section.dart';
|
||||
|
||||
class PendingInvoicesPage extends StatelessWidget {
|
||||
@@ -31,7 +32,7 @@ class PendingInvoicesPage extends StatelessWidget {
|
||||
|
||||
Widget _buildBody(BuildContext context, BillingState state) {
|
||||
if (state.status == BillingStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const InvoicesListSkeleton();
|
||||
}
|
||||
|
||||
if (state.pendingInvoices.isEmpty) {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer loading skeleton for the billing page content area.
|
||||
///
|
||||
/// Mimics the loaded layout with a pending invoices section,
|
||||
/// a spending breakdown card, and an invoice history list.
|
||||
class BillingPageSkeleton extends StatelessWidget {
|
||||
/// Creates a [BillingPageSkeleton].
|
||||
const BillingPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Pending invoices section header
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// Pending invoice cards
|
||||
const _InvoiceCardSkeleton(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const _InvoiceCardSkeleton(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Spending breakdown card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 160, height: 16),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
// Breakdown rows
|
||||
_BreakdownRowSkeleton(),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
_BreakdownRowSkeleton(),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
_BreakdownRowSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Invoice history section header
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
const UiShimmerListItem(),
|
||||
const UiShimmerListItem(),
|
||||
const UiShimmerListItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shimmer placeholder for a single pending invoice card.
|
||||
class _InvoiceCardSkeleton extends StatelessWidget {
|
||||
const _InvoiceCardSkeleton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
UiShimmerBox(
|
||||
width: 72,
|
||||
height: 24,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
const UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const UiShimmerLine(width: 200, height: 16),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
const UiShimmerLine(width: 160, height: 12),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 80, height: 10),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 100, height: 18),
|
||||
],
|
||||
),
|
||||
UiShimmerBox(
|
||||
width: 100,
|
||||
height: 36,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shimmer placeholder for a spending breakdown row.
|
||||
class _BreakdownRowSkeleton extends StatelessWidget {
|
||||
const _BreakdownRowSkeleton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
UiShimmerLine(width: 100, height: 14),
|
||||
UiShimmerLine(width: 60, height: 14),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer loading skeleton for invoice list pages.
|
||||
///
|
||||
/// Used by both [PendingInvoicesPage] and [InvoiceReadyPage] to show
|
||||
/// placeholder cards while data loads.
|
||||
class InvoicesListSkeleton extends StatelessWidget {
|
||||
/// Creates an [InvoicesListSkeleton].
|
||||
const InvoicesListSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: List.generate(4, (int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
UiShimmerBox(
|
||||
width: 64,
|
||||
height: 22,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
const UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const UiShimmerLine(width: 180, height: 16),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
const UiShimmerLine(width: 140, height: 12),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const Divider(color: UiColors.border),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 80, height: 10),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 100, height: 20),
|
||||
],
|
||||
),
|
||||
UiShimmerBox(
|
||||
width: 100,
|
||||
height: 36,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user