From 4423775fa150b2199ce04e6b64846d03a619129c Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 10 Mar 2026 14:25:56 -0400 Subject: [PATCH] feat: add shimmer loading skeletons for various pages and components - Implemented ReorderCardSkeleton and ReorderSectionSkeleton for the client home page. - Added SpendingCardSkeleton and SpendingSectionSkeleton for spending-related UI. - Created OrderCardSkeleton and associated skeletons for the view orders page. - Developed MetricCardSkeleton and MetricsGridSkeleton for reports page metrics. - Introduced HomePageSkeleton and its components for staff home page. - Added PaymentItemSkeleton and PaymentsPageSkeleton for payments page. - Created ShiftDetailsPageSkeleton and related components for shift details. - Implemented ShiftsPageSkeleton and ShiftCardSkeleton for shifts page. --- .../widgets/billing_page_skeleton.dart | 136 +------ .../billing_page_skeleton.dart | 67 ++++ .../breakdown_row_skeleton.dart | 19 + .../widgets/billing_page_skeleton/index.dart | 3 + .../invoice_card_skeleton.dart | 58 +++ .../widgets/coverage_page_skeleton.dart | 103 +----- .../coverage_page_skeleton.dart | 47 +++ .../widgets/coverage_page_skeleton/index.dart | 2 + .../shift_card_skeleton.dart | 60 ++++ .../widgets/client_home_body.dart | 1 - .../widgets/client_home_page_skeleton.dart | 339 +----------------- .../action_card_skeleton.dart | 28 ++ .../actions_section_skeleton.dart | 28 ++ .../client_home_page_skeleton.dart | 69 ++++ .../coverage_section_skeleton.dart | 30 ++ .../live_activity_section_skeleton.dart | 23 ++ .../metric_card_skeleton.dart | 33 ++ .../reorder_card_skeleton.dart | 63 ++++ .../reorder_section_skeleton.dart | 31 ++ .../spending_card_skeleton.dart | 47 +++ .../spending_section_skeleton.dart | 22 ++ .../widgets/view_orders_page_skeleton.dart | 212 +---------- .../view_orders_page_skeleton/index.dart | 5 + .../order_card_skeleton.dart | 127 +++++++ .../section_header_skeleton.dart | 24 ++ .../stat_divider_skeleton.dart | 17 + .../stat_item_skeleton.dart | 20 ++ .../view_orders_page_skeleton.dart | 41 +++ .../reports_page/metrics_grid_skeleton.dart | 72 +--- .../metrics_grid_skeleton/index.dart | 2 + .../metric_card_skeleton.dart | 45 +++ .../metrics_grid_skeleton.dart | 31 ++ .../widgets/home_page/home_page_skeleton.dart | 202 +---------- .../home_page_skeleton.dart | 66 ++++ .../home_page/home_page_skeleton/index.dart | 6 + .../quick_actions_skeleton.dart | 32 ++ .../recommended_section_skeleton.dart | 44 +++ .../shift_card_skeleton.dart | 37 ++ .../shift_section_skeleton.dart | 31 ++ .../home_page_skeleton/skeleton_divider.dart | 13 + .../widgets/payments_page_skeleton.dart | 149 +------- .../widgets/payments_page_skeleton/index.dart | 2 + .../payment_item_skeleton.dart | 40 +++ .../payments_page_skeleton.dart | 113 ++++++ .../widgets/shift_details_page_skeleton.dart | 174 +-------- .../shift_details_page_skeleton/index.dart | 2 + .../shift_details_page_skeleton.dart | 150 ++++++++ .../stat_card_skeleton.dart | 28 ++ .../widgets/shifts_page_skeleton.dart | 73 +--- .../widgets/shifts_page_skeleton/index.dart | 2 + .../shift_card_skeleton.dart | 44 +++ .../shifts_page_skeleton.dart | 33 ++ 52 files changed, 1603 insertions(+), 1443 deletions(-) create mode 100644 apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/billing_page_skeleton.dart create mode 100644 apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/breakdown_row_skeleton.dart create mode 100644 apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/invoice_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/coverage_page_skeleton.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/shift_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/action_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/actions_section_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/client_home_page_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/coverage_section_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/live_activity_section_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/metric_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_section_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_section_skeleton.dart create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/order_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/section_header_skeleton.dart create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_divider_skeleton.dart create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_item_skeleton.dart create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/view_orders_page_skeleton.dart create mode 100644 apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/index.dart create mode 100644 apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metric_card_skeleton.dart create mode 100644 apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metrics_grid_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/home_page_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/quick_actions_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/recommended_section_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_card_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_section_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/skeleton_divider.dart create mode 100644 apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payment_item_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payments_page_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/shift_details_page_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/stat_card_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/index.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shift_card_skeleton.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shifts_page_skeleton.dart diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton.dart index b5a64b74..398b9434 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton.dart @@ -1,135 +1 @@ -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), - ], - ); - } -} +export 'billing_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/billing_page_skeleton.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/billing_page_skeleton.dart new file mode 100644 index 00000000..e4d41037 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/billing_page_skeleton.dart @@ -0,0 +1,67 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'breakdown_row_skeleton.dart'; +import 'invoice_card_skeleton.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(), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/breakdown_row_skeleton.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/breakdown_row_skeleton.dart new file mode 100644 index 00000000..978b5f38 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/breakdown_row_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a spending breakdown row. +class BreakdownRowSkeleton extends StatelessWidget { + /// Creates a [BreakdownRowSkeleton]. + const BreakdownRowSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + UiShimmerLine(width: 100, height: 14), + UiShimmerLine(width: 60, height: 14), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/index.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/index.dart new file mode 100644 index 00000000..d803d599 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/index.dart @@ -0,0 +1,3 @@ +export 'billing_page_skeleton.dart'; +export 'breakdown_row_skeleton.dart'; +export 'invoice_card_skeleton.dart'; diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/invoice_card_skeleton.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/invoice_card_skeleton.dart new file mode 100644 index 00000000..e86811db --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/billing_page_skeleton/invoice_card_skeleton.dart @@ -0,0 +1,58 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single pending invoice card. +class InvoiceCardSkeleton extends StatelessWidget { + /// Creates an [InvoiceCardSkeleton]. + const InvoiceCardSkeleton({super.key}); + + @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, + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton.dart index 1efbc417..04c499bf 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton.dart @@ -1,102 +1 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton that mimics the coverage page loaded layout. -/// -/// Shows placeholder shapes for the quick stats row, shift section header, -/// and a list of shift cards with worker rows. -class CoveragePageSkeleton extends StatelessWidget { - /// Creates a [CoveragePageSkeleton]. - const CoveragePageSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return UiShimmer( - child: Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Quick stats row (2 stat cards) - const Row( - children: [ - Expanded(child: UiShimmerStatsCard()), - SizedBox(width: UiConstants.space2), - Expanded(child: UiShimmerStatsCard()), - ], - ), - const SizedBox(height: UiConstants.space6), - - // Shifts section header - const UiShimmerLine(width: 140, height: 18), - const SizedBox(height: UiConstants.space6), - - // Shift cards with worker rows - const _ShiftCardSkeleton(), - const SizedBox(height: UiConstants.space3), - const _ShiftCardSkeleton(), - const SizedBox(height: UiConstants.space3), - const _ShiftCardSkeleton(), - ], - ), - ), - ); - } -} - -/// Shimmer placeholder for a single shift card with header and worker rows. -class _ShiftCardSkeleton extends StatelessWidget { - const _ShiftCardSkeleton(); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: UiColors.border), - borderRadius: UiConstants.radiusLg, - ), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - // Shift header - Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerLine(width: 180, height: 16), - const SizedBox(height: UiConstants.space2), - const UiShimmerLine(width: 120, height: 12), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const UiShimmerLine(width: 80, height: 12), - const Spacer(), - UiShimmerBox( - width: 60, - height: 24, - borderRadius: UiConstants.radiusFull, - ), - ], - ), - ], - ), - ), - - // Worker rows - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - ).copyWith(bottom: UiConstants.space3), - child: const Column( - children: [ - UiShimmerListItem(), - UiShimmerListItem(), - ], - ), - ), - ], - ), - ); - } -} +export 'coverage_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/coverage_page_skeleton.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/coverage_page_skeleton.dart new file mode 100644 index 00000000..bfb12d31 --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/coverage_page_skeleton.dart @@ -0,0 +1,47 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'shift_card_skeleton.dart'; + +/// Shimmer loading skeleton that mimics the coverage page loaded layout. +/// +/// Shows placeholder shapes for the quick stats row, shift section header, +/// and a list of shift cards with worker rows. +class CoveragePageSkeleton extends StatelessWidget { + /// Creates a [CoveragePageSkeleton]. + const CoveragePageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quick stats row (2 stat cards) + const Row( + children: [ + Expanded(child: UiShimmerStatsCard()), + SizedBox(width: UiConstants.space2), + Expanded(child: UiShimmerStatsCard()), + ], + ), + const SizedBox(height: UiConstants.space6), + + // Shifts section header + const UiShimmerLine(width: 140, height: 18), + const SizedBox(height: UiConstants.space6), + + // Shift cards with worker rows + const ShiftCardSkeleton(), + const SizedBox(height: UiConstants.space3), + const ShiftCardSkeleton(), + const SizedBox(height: UiConstants.space3), + const ShiftCardSkeleton(), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/index.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/index.dart new file mode 100644 index 00000000..ddac4e8b --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/index.dart @@ -0,0 +1,2 @@ +export 'coverage_page_skeleton.dart'; +export 'shift_card_skeleton.dart'; diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/shift_card_skeleton.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/shift_card_skeleton.dart new file mode 100644 index 00000000..c74212cd --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_page_skeleton/shift_card_skeleton.dart @@ -0,0 +1,60 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single shift card with header and worker rows. +class ShiftCardSkeleton extends StatelessWidget { + /// Creates a [ShiftCardSkeleton]. + const ShiftCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + // Shift header + Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UiShimmerLine(width: 180, height: 16), + const SizedBox(height: UiConstants.space2), + const UiShimmerLine(width: 120, height: 12), + const SizedBox(height: UiConstants.space2), + Row( + children: [ + const UiShimmerLine(width: 80, height: 12), + const Spacer(), + UiShimmerBox( + width: 60, + height: 24, + borderRadius: UiConstants.radiusFull, + ), + ], + ), + ], + ), + ), + + // Worker rows + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + ).copyWith(bottom: UiConstants.space3), + child: const Column( + children: [ + UiShimmerListItem(), + UiShimmerListItem(), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_body.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_body.dart index 1da1bbdc..9b39ec2f 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_body.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_body.dart @@ -33,7 +33,6 @@ class ClientHomeBody extends StatelessWidget { } }, builder: (BuildContext context, ClientHomeState state) { - return const ClientHomePageSkeleton(); if (state.status == ClientHomeStatus.initial || state.status == ClientHomeStatus.loading) { return const ClientHomePageSkeleton(); diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton.dart index 806ca6da..c293fca1 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton.dart @@ -1,329 +1,10 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton for the client home page. -/// -/// Mimics the loaded dashboard layout with action cards, reorder cards, -/// coverage metrics, spending card, and live activity sections. -class ClientHomePageSkeleton extends StatelessWidget { - /// Creates a [ClientHomePageSkeleton]. - const ClientHomePageSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return UiShimmer( - child: ListView( - children: const [ - // Actions section - Padding( - padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), - child: _ActionsSectionSkeleton(), - ), - SizedBox(height: UiConstants.space8), - Divider(color: UiColors.border, height: 0.1), - SizedBox(height: UiConstants.space8), - - // Reorder section - Padding( - padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), - child: _ReorderSectionSkeleton(), - ), - SizedBox(height: UiConstants.space8), - Divider(color: UiColors.border, height: 0.1), - SizedBox(height: UiConstants.space8), - - // Coverage section - Padding( - padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), - child: _CoverageSectionSkeleton(), - ), - SizedBox(height: UiConstants.space8), - Divider(color: UiColors.border, height: 0.1), - SizedBox(height: UiConstants.space8), - - // Spending section - Padding( - padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), - child: _SpendingSectionSkeleton(), - ), - SizedBox(height: UiConstants.space8), - Divider(color: UiColors.border, height: 0.1), - SizedBox(height: UiConstants.space8), - - // Live activity section - Padding( - padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), - child: _LiveActivitySectionSkeleton(), - ), - SizedBox(height: UiConstants.space8), - ], - ), - ); - } -} - -/// Skeleton for the two side-by-side action cards. -class _ActionsSectionSkeleton extends StatelessWidget { - const _ActionsSectionSkeleton(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerSectionHeader(), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - Expanded(child: _ActionCardSkeleton()), - const SizedBox(width: UiConstants.space4), - Expanded(child: _ActionCardSkeleton()), - ], - ), - ], - ); - } -} - -/// Skeleton for a single action card with icon, title, and subtitle. -class _ActionCardSkeleton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border, width: 0.5), - borderRadius: UiConstants.radiusLg, - ), - child: const Column( - children: [ - UiShimmerBox(width: 36, height: 36), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 60, height: 14), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 100, height: 10), - ], - ), - ); - } -} - -/// Skeleton for the horizontal reorder cards list. -class _ReorderSectionSkeleton extends StatelessWidget { - const _ReorderSectionSkeleton(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerSectionHeader(), - const SizedBox(height: UiConstants.space2), - SizedBox( - height: 164, - child: Row( - children: [ - _ReorderCardSkeleton(), - const SizedBox(width: UiConstants.space3), - _ReorderCardSkeleton(), - ], - ), - ), - ], - ); - } -} - -/// Skeleton for a single reorder card. -class _ReorderCardSkeleton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container( - width: 260, - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border, width: 0.6), - borderRadius: UiConstants.radiusLg, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - UiShimmerBox(width: 36, height: 36), - SizedBox(width: UiConstants.space2), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerLine(width: 100, height: 14), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 80, height: 10), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - UiShimmerLine(width: 40, height: 14), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 60, height: 10), - ], - ), - ], - ), - const SizedBox(height: UiConstants.space3), - const Row( - children: [ - UiShimmerBox(width: 60, height: 22), - SizedBox(width: UiConstants.space2), - UiShimmerBox(width: 36, height: 22), - ], - ), - const Spacer(), - UiShimmerBox( - width: double.infinity, - height: 32, - borderRadius: UiConstants.radiusLg, - ), - ], - ), - ); - } -} - -/// Skeleton for the coverage metric cards row. -class _CoverageSectionSkeleton extends StatelessWidget { - const _CoverageSectionSkeleton(); - - @override - Widget build(BuildContext context) { - return const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerSectionHeader(), - SizedBox(height: UiConstants.space2), - Row( - children: [ - Expanded(child: _MetricCardSkeleton()), - SizedBox(width: UiConstants.space2), - Expanded(child: _MetricCardSkeleton()), - SizedBox(width: UiConstants.space2), - Expanded(child: _MetricCardSkeleton()), - ], - ), - ], - ); - } -} - -/// Skeleton for a single coverage metric card. -class _MetricCardSkeleton extends StatelessWidget { - const _MetricCardSkeleton(); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space2), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border, width: 0.5), - borderRadius: UiConstants.radiusLg, - ), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - UiShimmerCircle(size: 14), - SizedBox(width: UiConstants.space1), - UiShimmerLine(width: 40, height: 10), - ], - ), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 32, height: 20), - ], - ), - ); - } -} - -/// Skeleton for the spending gradient card. -class _SpendingSectionSkeleton extends StatelessWidget { - const _SpendingSectionSkeleton(); - - @override - Widget build(BuildContext context) { - return const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerSectionHeader(), - SizedBox(height: UiConstants.space2), - _SpendingCardSkeleton(), - ], - ); - } -} - -/// Skeleton mimicking the spending card layout. -class _SpendingCardSkeleton extends StatelessWidget { - const _SpendingCardSkeleton(); - - @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: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerLine(width: 60, height: 10), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 80, height: 22), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 50, height: 10), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - UiShimmerLine(width: 60, height: 10), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 70, height: 18), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 50, height: 10), - ], - ), - ), - ], - ), - ); - } -} - -/// Skeleton for the live activity section. -class _LiveActivitySectionSkeleton extends StatelessWidget { - const _LiveActivitySectionSkeleton(); - - @override - Widget build(BuildContext context) { - return const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerSectionHeader(), - SizedBox(height: UiConstants.space2), - UiShimmerStatsCard(), - SizedBox(height: UiConstants.space3), - UiShimmerListItem(), - UiShimmerListItem(), - ], - ); - } -} +export 'client_home_page_skeleton/action_card_skeleton.dart'; +export 'client_home_page_skeleton/actions_section_skeleton.dart'; +export 'client_home_page_skeleton/client_home_page_skeleton.dart'; +export 'client_home_page_skeleton/coverage_section_skeleton.dart'; +export 'client_home_page_skeleton/live_activity_section_skeleton.dart'; +export 'client_home_page_skeleton/metric_card_skeleton.dart'; +export 'client_home_page_skeleton/reorder_card_skeleton.dart'; +export 'client_home_page_skeleton/reorder_section_skeleton.dart'; +export 'client_home_page_skeleton/spending_card_skeleton.dart'; +export 'client_home_page_skeleton/spending_section_skeleton.dart'; diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/action_card_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/action_card_skeleton.dart new file mode 100644 index 00000000..dd4c0668 --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/action_card_skeleton.dart @@ -0,0 +1,28 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for a single action card with icon, title, and subtitle. +class ActionCardSkeleton extends StatelessWidget { + /// Creates an [ActionCardSkeleton]. + const ActionCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border, width: 0.5), + borderRadius: UiConstants.radiusLg, + ), + child: const Column( + children: [ + UiShimmerBox(width: 36, height: 36), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 60, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 100, height: 10), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/actions_section_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/actions_section_skeleton.dart new file mode 100644 index 00000000..4aafa370 --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/actions_section_skeleton.dart @@ -0,0 +1,28 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'action_card_skeleton.dart'; + +/// Skeleton for the two side-by-side action cards. +class ActionsSectionSkeleton extends StatelessWidget { + /// Creates an [ActionsSectionSkeleton]. + const ActionsSectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerSectionHeader(), + SizedBox(height: UiConstants.space2), + Row( + children: [ + Expanded(child: ActionCardSkeleton()), + SizedBox(width: UiConstants.space4), + Expanded(child: ActionCardSkeleton()), + ], + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/client_home_page_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/client_home_page_skeleton.dart new file mode 100644 index 00000000..09cddb61 --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/client_home_page_skeleton.dart @@ -0,0 +1,69 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'actions_section_skeleton.dart'; +import 'coverage_section_skeleton.dart'; +import 'live_activity_section_skeleton.dart'; +import 'reorder_section_skeleton.dart'; +import 'spending_section_skeleton.dart'; + +/// Shimmer loading skeleton for the client home page. +/// +/// Mimics the loaded dashboard layout with action cards, reorder cards, +/// coverage metrics, spending card, and live activity sections. +class ClientHomePageSkeleton extends StatelessWidget { + /// Creates a [ClientHomePageSkeleton]. + const ClientHomePageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: ListView( + children: const [ + // Actions section + Padding( + padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), + child: ActionsSectionSkeleton(), + ), + SizedBox(height: UiConstants.space8), + Divider(color: UiColors.border, height: 0.1), + SizedBox(height: UiConstants.space8), + + // Reorder section + Padding( + padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), + child: ReorderSectionSkeleton(), + ), + SizedBox(height: UiConstants.space8), + Divider(color: UiColors.border, height: 0.1), + SizedBox(height: UiConstants.space8), + + // Coverage section + Padding( + padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), + child: CoverageSectionSkeleton(), + ), + SizedBox(height: UiConstants.space8), + Divider(color: UiColors.border, height: 0.1), + SizedBox(height: UiConstants.space8), + + // Spending section + Padding( + padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), + child: SpendingSectionSkeleton(), + ), + SizedBox(height: UiConstants.space8), + Divider(color: UiColors.border, height: 0.1), + SizedBox(height: UiConstants.space8), + + // Live activity section + Padding( + padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), + child: LiveActivitySectionSkeleton(), + ), + SizedBox(height: UiConstants.space8), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/coverage_section_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/coverage_section_skeleton.dart new file mode 100644 index 00000000..628d6489 --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/coverage_section_skeleton.dart @@ -0,0 +1,30 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'metric_card_skeleton.dart'; + +/// Skeleton for the coverage metric cards row. +class CoverageSectionSkeleton extends StatelessWidget { + /// Creates a [CoverageSectionSkeleton]. + const CoverageSectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerSectionHeader(), + SizedBox(height: UiConstants.space2), + Row( + children: [ + Expanded(child: MetricCardSkeleton()), + SizedBox(width: UiConstants.space2), + Expanded(child: MetricCardSkeleton()), + SizedBox(width: UiConstants.space2), + Expanded(child: MetricCardSkeleton()), + ], + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/live_activity_section_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/live_activity_section_skeleton.dart new file mode 100644 index 00000000..0abe8950 --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/live_activity_section_skeleton.dart @@ -0,0 +1,23 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for the live activity section. +class LiveActivitySectionSkeleton extends StatelessWidget { + /// Creates a [LiveActivitySectionSkeleton]. + const LiveActivitySectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerSectionHeader(), + SizedBox(height: UiConstants.space2), + UiShimmerStatsCard(), + SizedBox(height: UiConstants.space3), + UiShimmerListItem(), + UiShimmerListItem(), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/metric_card_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/metric_card_skeleton.dart new file mode 100644 index 00000000..bb154f8d --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/metric_card_skeleton.dart @@ -0,0 +1,33 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for a single coverage metric card. +class MetricCardSkeleton extends StatelessWidget { + /// Creates a [MetricCardSkeleton]. + const MetricCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space2), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border, width: 0.5), + borderRadius: UiConstants.radiusLg, + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + UiShimmerCircle(size: 14), + SizedBox(width: UiConstants.space1), + UiShimmerLine(width: 40, height: 10), + ], + ), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 32, height: 20), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_card_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_card_skeleton.dart new file mode 100644 index 00000000..b07431ad --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_card_skeleton.dart @@ -0,0 +1,63 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for a single reorder card. +class ReorderCardSkeleton extends StatelessWidget { + /// Creates a [ReorderCardSkeleton]. + const ReorderCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 260, + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border, width: 0.6), + borderRadius: UiConstants.radiusLg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + UiShimmerBox(width: 36, height: 36), + SizedBox(width: UiConstants.space2), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 100, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 80, height: 10), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + UiShimmerLine(width: 40, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 60, height: 10), + ], + ), + ], + ), + const SizedBox(height: UiConstants.space3), + const Row( + children: [ + UiShimmerBox(width: 60, height: 22), + SizedBox(width: UiConstants.space2), + UiShimmerBox(width: 36, height: 22), + ], + ), + const Spacer(), + UiShimmerBox( + width: double.infinity, + height: 32, + borderRadius: UiConstants.radiusLg, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_section_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_section_skeleton.dart new file mode 100644 index 00000000..0e292f5a --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/reorder_section_skeleton.dart @@ -0,0 +1,31 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'reorder_card_skeleton.dart'; + +/// Skeleton for the horizontal reorder cards list. +class ReorderSectionSkeleton extends StatelessWidget { + /// Creates a [ReorderSectionSkeleton]. + const ReorderSectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerSectionHeader(), + SizedBox(height: UiConstants.space2), + SizedBox( + height: 164, + child: Row( + children: [ + ReorderCardSkeleton(), + SizedBox(width: UiConstants.space3), + ReorderCardSkeleton(), + ], + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_card_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_card_skeleton.dart new file mode 100644 index 00000000..dee41bff --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_card_skeleton.dart @@ -0,0 +1,47 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton mimicking the spending card layout. +class SpendingCardSkeleton extends StatelessWidget { + /// Creates a [SpendingCardSkeleton]. + const SpendingCardSkeleton({super.key}); + + @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: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 60, height: 10), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 80, height: 22), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 50, height: 10), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + UiShimmerLine(width: 60, height: 10), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 70, height: 18), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 50, height: 10), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_section_skeleton.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_section_skeleton.dart new file mode 100644 index 00000000..c46a7e2a --- /dev/null +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_page_skeleton/spending_section_skeleton.dart @@ -0,0 +1,22 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'spending_card_skeleton.dart'; + +/// Skeleton for the spending gradient card. +class SpendingSectionSkeleton extends StatelessWidget { + /// Creates a [SpendingSectionSkeleton]. + const SpendingSectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerSectionHeader(), + SizedBox(height: UiConstants.space2), + SpendingCardSkeleton(), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton.dart index 3ae3ab64..66f9a6da 100644 --- a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton.dart +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton.dart @@ -1,211 +1 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton for the View Orders page. -/// -/// Mimics the loaded layout: a section header followed by a list of order -/// card placeholders, each containing badge, title, location, stats, time -/// boxes, and a coverage progress bar. -class ViewOrdersPageSkeleton extends StatelessWidget { - /// Creates a [ViewOrdersPageSkeleton]. - const ViewOrdersPageSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return UiShimmer( - child: ListView( - padding: const EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space4, - UiConstants.space5, - // Extra bottom padding for bottom navigation clearance. - UiConstants.space24, - ), - children: [ - // Section header placeholder (dot + title + count) - const _SectionHeaderSkeleton(), - // Order card placeholders - ...List.generate(3, (int index) { - return const Padding( - padding: EdgeInsets.only(bottom: UiConstants.space3), - child: _OrderCardSkeleton(), - ); - }), - ], - ), - ); - } -} - -/// Shimmer placeholder for the section header row. -class _SectionHeaderSkeleton extends StatelessWidget { - const _SectionHeaderSkeleton(); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(bottom: UiConstants.space3), - child: Row( - children: [ - UiShimmerCircle(size: 8), - SizedBox(width: UiConstants.space2), - UiShimmerLine(width: 100, height: 14), - SizedBox(width: UiConstants.space1), - UiShimmerLine(width: 24, height: 14), - ], - ), - ); - } -} - -/// Shimmer placeholder for a single order card. -class _OrderCardSkeleton extends StatelessWidget { - const _OrderCardSkeleton(); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: UiColors.border, width: 0.5), - borderRadius: UiConstants.radiusLg, - ), - child: Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Status and type badges - Row( - children: [ - UiShimmerBox( - width: 80, - height: 22, - borderRadius: UiConstants.radiusSm, - ), - const SizedBox(width: UiConstants.space2), - UiShimmerBox( - width: 72, - height: 22, - borderRadius: UiConstants.radiusSm, - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - // Title line - const UiShimmerLine(width: 200, height: 18), - const SizedBox(height: UiConstants.space2), - - // Event name line - const UiShimmerLine(width: 160, height: 14), - const SizedBox(height: UiConstants.space4), - - // Location lines - const Row( - children: [ - UiShimmerCircle(size: 14), - SizedBox(width: UiConstants.space2), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerLine(width: 180, height: 12), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 140, height: 10), - ], - ), - ), - ], - ), - - const SizedBox(height: UiConstants.space4), - const Divider(height: 1, color: UiColors.border), - const SizedBox(height: UiConstants.space4), - - // Stats row (cost / hours / workers) - const Padding( - padding: EdgeInsets.symmetric( - horizontal: UiConstants.space4, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _StatItemSkeleton(), - _StatDividerSkeleton(), - _StatItemSkeleton(), - _StatDividerSkeleton(), - _StatItemSkeleton(), - ], - ), - ), - - const SizedBox(height: UiConstants.space5), - - // Time boxes (clock in / clock out) - Row( - children: [ - Expanded(child: _timeBoxSkeleton()), - const SizedBox(width: UiConstants.space3), - Expanded(child: _timeBoxSkeleton()), - ], - ), - - const SizedBox(height: UiConstants.space4), - - // Coverage progress bar - const UiShimmerLine(height: 8), - ], - ), - ), - ); - } - - /// Builds a placeholder for a time display box (clock-in / clock-out). - Widget _timeBoxSkeleton() { - return Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border, width: 0.5), - borderRadius: UiConstants.radiusLg, - ), - child: const Column( - children: [ - UiShimmerLine(width: 60, height: 10), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 80, height: 16), - ], - ), - ); - } -} - -/// Shimmer placeholder for a single stat item (icon + value + label). -class _StatItemSkeleton extends StatelessWidget { - const _StatItemSkeleton(); - - @override - Widget build(BuildContext context) { - return const Column( - spacing: UiConstants.space1, - children: [ - UiShimmerCircle(size: 14), - UiShimmerLine(width: 32, height: 16), - UiShimmerLine(width: 40, height: 10), - ], - ); - } -} - -/// Shimmer placeholder for the vertical stat divider. -class _StatDividerSkeleton extends StatelessWidget { - const _StatDividerSkeleton(); - - @override - Widget build(BuildContext context) { - return const UiShimmerBox( - width: 1, - height: 24, - borderRadius: BorderRadius.zero, - ); - } -} +export 'view_orders_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/index.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/index.dart new file mode 100644 index 00000000..d64c5a98 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/index.dart @@ -0,0 +1,5 @@ +export 'order_card_skeleton.dart'; +export 'section_header_skeleton.dart'; +export 'stat_divider_skeleton.dart'; +export 'stat_item_skeleton.dart'; +export 'view_orders_page_skeleton.dart'; diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/order_card_skeleton.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/order_card_skeleton.dart new file mode 100644 index 00000000..8f1cf480 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/order_card_skeleton.dart @@ -0,0 +1,127 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'stat_divider_skeleton.dart'; +import 'stat_item_skeleton.dart'; + +/// Shimmer placeholder for a single order card. +class OrderCardSkeleton extends StatelessWidget { + /// Creates an [OrderCardSkeleton]. + const OrderCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: UiColors.border, width: 0.5), + borderRadius: UiConstants.radiusLg, + ), + child: Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status and type badges + Row( + children: [ + UiShimmerBox( + width: 80, + height: 22, + borderRadius: UiConstants.radiusSm, + ), + const SizedBox(width: UiConstants.space2), + UiShimmerBox( + width: 72, + height: 22, + borderRadius: UiConstants.radiusSm, + ), + ], + ), + const SizedBox(height: UiConstants.space3), + + // Title line + const UiShimmerLine(width: 200, height: 18), + const SizedBox(height: UiConstants.space2), + + // Event name line + const UiShimmerLine(width: 160, height: 14), + const SizedBox(height: UiConstants.space4), + + // Location lines + const Row( + children: [ + UiShimmerCircle(size: 14), + SizedBox(width: UiConstants.space2), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 180, height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 140, height: 10), + ], + ), + ), + ], + ), + + const SizedBox(height: UiConstants.space4), + const Divider(height: 1, color: UiColors.border), + const SizedBox(height: UiConstants.space4), + + // Stats row (cost / hours / workers) + const Padding( + padding: EdgeInsets.symmetric( + horizontal: UiConstants.space4, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StatItemSkeleton(), + StatDividerSkeleton(), + StatItemSkeleton(), + StatDividerSkeleton(), + StatItemSkeleton(), + ], + ), + ), + + const SizedBox(height: UiConstants.space5), + + // Time boxes (clock in / clock out) + Row( + children: [ + Expanded(child: _timeBoxSkeleton()), + const SizedBox(width: UiConstants.space3), + Expanded(child: _timeBoxSkeleton()), + ], + ), + + const SizedBox(height: UiConstants.space4), + + // Coverage progress bar + const UiShimmerLine(height: 8), + ], + ), + ), + ); + } + + /// Builds a placeholder for a time display box (clock-in / clock-out). + Widget _timeBoxSkeleton() { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border, width: 0.5), + borderRadius: UiConstants.radiusLg, + ), + child: const Column( + children: [ + UiShimmerLine(width: 60, height: 10), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 80, height: 16), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/section_header_skeleton.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/section_header_skeleton.dart new file mode 100644 index 00000000..491b0c60 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/section_header_skeleton.dart @@ -0,0 +1,24 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the section header row (dot + title + count). +class SectionHeaderSkeleton extends StatelessWidget { + /// Creates a [SectionHeaderSkeleton]. + const SectionHeaderSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.only(bottom: UiConstants.space3), + child: Row( + children: [ + UiShimmerCircle(size: 8), + SizedBox(width: UiConstants.space2), + UiShimmerLine(width: 100, height: 14), + SizedBox(width: UiConstants.space1), + UiShimmerLine(width: 24, height: 14), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_divider_skeleton.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_divider_skeleton.dart new file mode 100644 index 00000000..b7b0878d --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_divider_skeleton.dart @@ -0,0 +1,17 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the vertical stat divider. +class StatDividerSkeleton extends StatelessWidget { + /// Creates a [StatDividerSkeleton]. + const StatDividerSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const UiShimmerBox( + width: 1, + height: 24, + borderRadius: BorderRadius.zero, + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_item_skeleton.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_item_skeleton.dart new file mode 100644 index 00000000..85cbe602 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/stat_item_skeleton.dart @@ -0,0 +1,20 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single stat item (icon + value + label). +class StatItemSkeleton extends StatelessWidget { + /// Creates a [StatItemSkeleton]. + const StatItemSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + spacing: UiConstants.space1, + children: [ + UiShimmerCircle(size: 14), + UiShimmerLine(width: 32, height: 16), + UiShimmerLine(width: 40, height: 10), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/view_orders_page_skeleton.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/view_orders_page_skeleton.dart new file mode 100644 index 00000000..87f45b7d --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_orders_page_skeleton/view_orders_page_skeleton.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'order_card_skeleton.dart'; +import 'section_header_skeleton.dart'; + +/// Shimmer loading skeleton for the View Orders page. +/// +/// Mimics the loaded layout: a section header followed by a list of order +/// card placeholders, each containing badge, title, location, stats, time +/// boxes, and a coverage progress bar. +class ViewOrdersPageSkeleton extends StatelessWidget { + /// Creates a [ViewOrdersPageSkeleton]. + const ViewOrdersPageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: ListView( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space4, + UiConstants.space5, + // Extra bottom padding for bottom navigation clearance. + UiConstants.space24, + ), + children: [ + // Section header placeholder (dot + title + count) + const SectionHeaderSkeleton(), + // Order card placeholders + ...List.generate(3, (int index) { + return const Padding( + padding: EdgeInsets.only(bottom: UiConstants.space3), + child: OrderCardSkeleton(), + ); + }), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton.dart index 52717048..0bebed71 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton.dart @@ -1,71 +1 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton for the reports metrics grid. -/// -/// Shows a 2-column grid of 6 placeholder cards matching the [MetricsGrid] -/// loaded layout. -class MetricsGridSkeleton extends StatelessWidget { - /// Creates a [MetricsGridSkeleton]. - const MetricsGridSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return UiShimmer( - child: GridView.count( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space6), - crossAxisCount: 2, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: UiConstants.space3, - crossAxisSpacing: UiConstants.space3, - childAspectRatio: 1.32, - children: List.generate(6, (int index) { - return const _MetricCardSkeleton(); - }), - ), - ); - } -} - -/// Shimmer placeholder for a single metric card. -class _MetricCardSkeleton extends StatelessWidget { - const _MetricCardSkeleton(); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border), - borderRadius: UiConstants.radiusLg, - color: UiColors.cardViewBackground, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Icon + label row - Row( - children: [ - const UiShimmerCircle(size: UiConstants.space6), - const SizedBox(width: UiConstants.space2), - const Expanded( - child: UiShimmerLine(width: 60, height: 10), - ), - ], - ), - const Spacer(), - // Value - const UiShimmerLine(width: 80, height: 22), - const SizedBox(height: UiConstants.space2), - // Badge - UiShimmerBox( - width: 60, - height: 20, - borderRadius: UiConstants.radiusSm, - ), - ], - ), - ); - } -} +export 'metrics_grid_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/index.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/index.dart new file mode 100644 index 00000000..41c2aebd --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/index.dart @@ -0,0 +1,2 @@ +export 'metric_card_skeleton.dart'; +export 'metrics_grid_skeleton.dart'; diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metric_card_skeleton.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metric_card_skeleton.dart new file mode 100644 index 00000000..61d5940d --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metric_card_skeleton.dart @@ -0,0 +1,45 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single metric card. +class MetricCardSkeleton extends StatelessWidget { + /// Creates a [MetricCardSkeleton]. + const MetricCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon + label row + Row( + children: [ + const UiShimmerCircle(size: UiConstants.space6), + const SizedBox(width: UiConstants.space2), + const Expanded( + child: UiShimmerLine(width: 60, height: 10), + ), + ], + ), + const Spacer(), + // Value + const UiShimmerLine(width: 80, height: 22), + const SizedBox(height: UiConstants.space2), + // Badge + UiShimmerBox( + width: 60, + height: 20, + borderRadius: UiConstants.radiusSm, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metrics_grid_skeleton.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metrics_grid_skeleton.dart new file mode 100644 index 00000000..9181ec7a --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid_skeleton/metrics_grid_skeleton.dart @@ -0,0 +1,31 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'metric_card_skeleton.dart'; + +/// Shimmer loading skeleton for the reports metrics grid. +/// +/// Shows a 2-column grid of 6 placeholder cards matching the [MetricsGrid] +/// loaded layout. +class MetricsGridSkeleton extends StatelessWidget { + /// Creates a [MetricsGridSkeleton]. + const MetricsGridSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: GridView.count( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space6), + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: UiConstants.space3, + crossAxisSpacing: UiConstants.space3, + childAspectRatio: 1.32, + children: List.generate(6, (int index) { + return const MetricCardSkeleton(); + }), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton.dart index aaa8e48c..652a6c58 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton.dart @@ -1,201 +1 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton for the staff home page. -/// -/// Mimics the loaded layout with quick actions, today's shifts, tomorrow's -/// shifts, recommended shifts, and benefits sections. Displayed while -/// [HomeCubit] is fetching initial data. -class HomePageSkeleton extends StatelessWidget { - /// Creates a [HomePageSkeleton]. - const HomePageSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return UiShimmer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Quick actions row (3 circular icons + labels) - const _QuickActionsSkeleton(), - - const _SkeletonDivider(), - - // Today's Shifts section - const _ShiftSectionSkeleton(), - - const _SkeletonDivider(), - - // Tomorrow's Shifts section - const _ShiftSectionSkeleton(), - - const _SkeletonDivider(), - - // Recommended Shifts (horizontal cards) - const _RecommendedSectionSkeleton(), - - const _SkeletonDivider(), - - // Benefits section - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerSectionHeader(), - const SizedBox(height: UiConstants.space3), - UiShimmerList( - itemCount: 2, - itemBuilder: (index) => const UiShimmerListItem(), - ), - ], - ), - ), - ], - ), - ); - } -} - -/// Skeleton for the quick actions row (3 circular placeholders with labels). -class _QuickActionsSkeleton extends StatelessWidget { - const _QuickActionsSkeleton(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(3, (index) { - return const Expanded( - child: Column( - children: [ - UiShimmerCircle(size: 48), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 60, height: 12), - ], - ), - ); - }), - ), - ); - } -} - -/// Skeleton for a shift section (section header + 2 shift card placeholders). -class _ShiftSectionSkeleton extends StatelessWidget { - const _ShiftSectionSkeleton(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerSectionHeader(), - const SizedBox(height: UiConstants.space3), - UiShimmerList( - itemCount: 2, - itemBuilder: (index) => const _ShiftCardSkeleton(), - ), - ], - ), - ); - } -} - -/// Skeleton for a single compact shift card on the home page. -class _ShiftCardSkeleton extends StatelessWidget { - const _ShiftCardSkeleton(); - - @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: [ - UiShimmerBox(width: 48, height: 48), - SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerLine(width: 160, height: 14), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 120, height: 12), - ], - ), - ), - SizedBox(width: UiConstants.space3), - UiShimmerBox(width: 56, height: 24), - ], - ), - ); - } -} - -/// Skeleton for the recommended shifts horizontal scroll section. -class _RecommendedSectionSkeleton extends StatelessWidget { - const _RecommendedSectionSkeleton(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), - child: UiShimmerSectionHeader(), - ), - const SizedBox(height: UiConstants.space3), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - ), - itemCount: 3, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.only(right: UiConstants.space3), - child: UiShimmerBox( - width: 200, - height: 120, - borderRadius: UiConstants.radiusLg, - ), - ), - ), - ), - ], - ), - ); - } -} - -/// A thin full-width divider placeholder matching the home page layout. -class _SkeletonDivider extends StatelessWidget { - const _SkeletonDivider(); - - @override - Widget build(BuildContext context) { - return const Divider(height: 1, thickness: 0.5, color: UiColors.border); - } -} +export 'home_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/home_page_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/home_page_skeleton.dart new file mode 100644 index 00000000..2892b948 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/home_page_skeleton.dart @@ -0,0 +1,66 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'quick_actions_skeleton.dart'; +import 'recommended_section_skeleton.dart'; +import 'shift_section_skeleton.dart'; +import 'skeleton_divider.dart'; + +/// Shimmer loading skeleton for the staff home page. +/// +/// Mimics the loaded layout with quick actions, today's shifts, tomorrow's +/// shifts, recommended shifts, and benefits sections. Displayed while +/// [HomeCubit] is fetching initial data. +class HomePageSkeleton extends StatelessWidget { + /// Creates a [HomePageSkeleton]. + const HomePageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quick actions row (3 circular icons + labels) + const QuickActionsSkeleton(), + + const SkeletonDivider(), + + // Today's Shifts section + const ShiftSectionSkeleton(), + + const SkeletonDivider(), + + // Tomorrow's Shifts section + const ShiftSectionSkeleton(), + + const SkeletonDivider(), + + // Recommended Shifts (horizontal cards) + const RecommendedSectionSkeleton(), + + const SkeletonDivider(), + + // Benefits section + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 2, + itemBuilder: (index) => const UiShimmerListItem(), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/index.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/index.dart new file mode 100644 index 00000000..bb80e1c9 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/index.dart @@ -0,0 +1,6 @@ +export 'home_page_skeleton.dart'; +export 'quick_actions_skeleton.dart'; +export 'recommended_section_skeleton.dart'; +export 'shift_card_skeleton.dart'; +export 'shift_section_skeleton.dart'; +export 'skeleton_divider.dart'; diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/quick_actions_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/quick_actions_skeleton.dart new file mode 100644 index 00000000..b7dc048c --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/quick_actions_skeleton.dart @@ -0,0 +1,32 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for the quick actions row (3 circular placeholders with labels). +class QuickActionsSkeleton extends StatelessWidget { + /// Creates a [QuickActionsSkeleton]. + const QuickActionsSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(3, (index) { + return const Expanded( + child: Column( + children: [ + UiShimmerCircle(size: 48), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 60, height: 12), + ], + ), + ); + }), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/recommended_section_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/recommended_section_skeleton.dart new file mode 100644 index 00000000..15cd2ffe --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/recommended_section_skeleton.dart @@ -0,0 +1,44 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for the recommended shifts horizontal scroll section. +class RecommendedSectionSkeleton extends StatelessWidget { + /// Creates a [RecommendedSectionSkeleton]. + const RecommendedSectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: UiConstants.space4), + child: UiShimmerSectionHeader(), + ), + const SizedBox(height: UiConstants.space3), + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + ), + itemCount: 3, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(right: UiConstants.space3), + child: UiShimmerBox( + width: 200, + height: 120, + borderRadius: UiConstants.radiusLg, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_card_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_card_skeleton.dart new file mode 100644 index 00000000..450aea7d --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_card_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for a single compact shift card on the home page. +class ShiftCardSkeleton extends StatelessWidget { + /// Creates a [ShiftCardSkeleton]. + const ShiftCardSkeleton({super.key}); + + @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: [ + UiShimmerBox(width: 48, height: 48), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 160, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 120, height: 12), + ], + ), + ), + SizedBox(width: UiConstants.space3), + UiShimmerBox(width: 56, height: 24), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_section_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_section_skeleton.dart new file mode 100644 index 00000000..f8ffc72a --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/shift_section_skeleton.dart @@ -0,0 +1,31 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'shift_card_skeleton.dart'; + +/// Skeleton for a shift section (section header + 2 shift card placeholders). +class ShiftSectionSkeleton extends StatelessWidget { + /// Creates a [ShiftSectionSkeleton]. + const ShiftSectionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 2, + itemBuilder: (index) => const ShiftCardSkeleton(), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/skeleton_divider.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/skeleton_divider.dart new file mode 100644 index 00000000..51f0566c --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_page_skeleton/skeleton_divider.dart @@ -0,0 +1,13 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A thin full-width divider placeholder matching the home page layout. +class SkeletonDivider extends StatelessWidget { + /// Creates a [SkeletonDivider]. + const SkeletonDivider({super.key}); + + @override + Widget build(BuildContext context) { + return const Divider(height: 1, thickness: 0.5, color: UiColors.border); + } +} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton.dart index abeeeb0a..f6d4c461 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton.dart @@ -1,148 +1 @@ -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), - ], - ), - ); - } -} +export 'payments_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/index.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/index.dart new file mode 100644 index 00000000..ec96faf5 --- /dev/null +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/index.dart @@ -0,0 +1,2 @@ +export 'payment_item_skeleton.dart'; +export 'payments_page_skeleton.dart'; diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payment_item_skeleton.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payment_item_skeleton.dart new file mode 100644 index 00000000..2d24c1ae --- /dev/null +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payment_item_skeleton.dart @@ -0,0 +1,40 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// 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 { + /// Creates a [PaymentItemSkeleton]. + const PaymentItemSkeleton({super.key}); + + @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), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payments_page_skeleton.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payments_page_skeleton.dart new file mode 100644 index 00000000..45de7a7a --- /dev/null +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payments_page_skeleton/payments_page_skeleton.dart @@ -0,0 +1,113 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'payment_item_skeleton.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(), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton.dart index 01bdefeb..85f9f266 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton.dart @@ -1,173 +1 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton for the shift details page. -/// -/// Mimics the loaded layout: a header with icon + text lines, a stats row -/// with three stat cards, and content sections with date/time and location -/// placeholders. -class ShiftDetailsPageSkeleton extends StatelessWidget { - /// Creates a [ShiftDetailsPageSkeleton]. - const ShiftDetailsPageSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: const UiAppBar(centerTitle: false), - body: UiShimmer( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header: icon box + title/subtitle lines - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerBox( - width: 114, - height: 100, - borderRadius: UiConstants.radiusMd, - ), - const SizedBox(width: UiConstants.space4), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UiShimmerLine(width: 180, height: 20), - SizedBox(height: UiConstants.space3), - UiShimmerLine(width: 140, height: 14), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 200, height: 12), - ], - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Stats row: three stat cards - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: List.generate(3, (index) { - return Expanded( - child: Padding( - padding: EdgeInsets.only( - left: index > 0 ? UiConstants.space2 : 0, - ), - child: const _StatCardSkeleton(), - ), - ); - }), - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Date / time section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerLine(width: 100, height: 14), - const SizedBox(height: UiConstants.space3), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - UiShimmerLine(width: 80, height: 12), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 120, height: 16), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - UiShimmerLine(width: 80, height: 12), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 120, height: 16), - ], - ), - ), - ], - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Location section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - UiShimmerLine(width: 80, height: 14), - SizedBox(height: UiConstants.space3), - UiShimmerLine(height: 14), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 240, height: 12), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Description section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - UiShimmerLine(width: 120, height: 14), - SizedBox(height: UiConstants.space3), - UiShimmerLine(height: 12), - SizedBox(height: UiConstants.space2), - UiShimmerLine(height: 12), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 200, height: 12), - ], - ), - ), - ], - ), - ), - ), - ); - } -} - -/// Skeleton for a single stat card in the stats row. -class _StatCardSkeleton extends StatelessWidget { - const _StatCardSkeleton(); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.bgThird, - borderRadius: UiConstants.radiusMd, - ), - child: const Column( - children: [ - UiShimmerCircle(size: 40), - SizedBox(height: UiConstants.space2), - UiShimmerLine(width: 50, height: 16), - SizedBox(height: UiConstants.space1), - UiShimmerLine(width: 60, height: 12), - ], - ), - ); - } -} +export 'shift_details_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/index.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/index.dart new file mode 100644 index 00000000..01ee6e4c --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/index.dart @@ -0,0 +1,2 @@ +export 'shift_details_page_skeleton.dart'; +export 'stat_card_skeleton.dart'; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/shift_details_page_skeleton.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/shift_details_page_skeleton.dart new file mode 100644 index 00000000..dbb787f9 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/shift_details_page_skeleton.dart @@ -0,0 +1,150 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'stat_card_skeleton.dart'; + +/// Shimmer loading skeleton for the shift details page. +/// +/// Mimics the loaded layout: a header with icon + text lines, a stats row +/// with three stat cards, and content sections with date/time and location +/// placeholders. +class ShiftDetailsPageSkeleton extends StatelessWidget { + /// Creates a [ShiftDetailsPageSkeleton]. + const ShiftDetailsPageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const UiAppBar(centerTitle: false), + body: UiShimmer( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: icon box + title/subtitle lines + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerBox( + width: 114, + height: 100, + borderRadius: UiConstants.radiusMd, + ), + const SizedBox(width: UiConstants.space4), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 180, height: 20), + SizedBox(height: UiConstants.space3), + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 200, height: 12), + ], + ), + ), + ], + ), + ), + + const Divider(height: 1, thickness: 0.5), + + // Stats row: three stat cards + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: List.generate(3, (index) { + return Expanded( + child: Padding( + padding: EdgeInsets.only( + left: index > 0 ? UiConstants.space2 : 0, + ), + child: const StatCardSkeleton(), + ), + ); + }), + ), + ), + + const Divider(height: 1, thickness: 0.5), + + // Date / time section + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UiShimmerLine(width: 100, height: 14), + const SizedBox(height: UiConstants.space3), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerLine(width: 80, height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 120, height: 16), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerLine(width: 80, height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 120, height: 16), + ], + ), + ), + ], + ), + ], + ), + ), + + const Divider(height: 1, thickness: 0.5), + + // Location section + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerLine(width: 80, height: 14), + SizedBox(height: UiConstants.space3), + UiShimmerLine(height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 240, height: 12), + ], + ), + ), + + const Divider(height: 1, thickness: 0.5), + + // Description section + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerLine(width: 120, height: 14), + SizedBox(height: UiConstants.space3), + UiShimmerLine(height: 12), + SizedBox(height: UiConstants.space2), + UiShimmerLine(height: 12), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 200, height: 12), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/stat_card_skeleton.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/stat_card_skeleton.dart new file mode 100644 index 00000000..595a02b1 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details_page_skeleton/stat_card_skeleton.dart @@ -0,0 +1,28 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for a single stat card in the stats row. +class StatCardSkeleton extends StatelessWidget { + /// Creates a [StatCardSkeleton]. + const StatCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgThird, + borderRadius: UiConstants.radiusMd, + ), + child: const Column( + children: [ + UiShimmerCircle(size: 40), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 50, height: 16), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 60, height: 12), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton.dart index fb187171..e105af4b 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton.dart @@ -1,72 +1 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// Shimmer loading skeleton for the shifts page body content. -/// -/// Mimics the loaded layout with a section header and a list of shift card -/// placeholders. Used while the initial shifts data is being fetched. -class ShiftsPageSkeleton extends StatelessWidget { - /// Creates a [ShiftsPageSkeleton]. - const ShiftsPageSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return UiShimmer( - child: Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const UiShimmerSectionHeader(), - const SizedBox(height: UiConstants.space3), - UiShimmerList( - itemCount: 5, - itemBuilder: (index) => const _ShiftCardSkeleton(), - ), - ], - ), - ), - ); - } -} - -/// Skeleton for a single shift card matching the shift list item layout. -/// -/// Shows a rounded container with placeholder lines for the shift title, -/// time, location, and a trailing status badge. -class _ShiftCardSkeleton extends StatelessWidget { - const _ShiftCardSkeleton(); - - @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( - children: [ - const Expanded( - child: UiShimmerLine(width: 180, height: 16), - ), - const SizedBox(width: UiConstants.space3), - UiShimmerBox( - width: 64, - height: 24, - borderRadius: UiConstants.radiusFull, - ), - ], - ), - const SizedBox(height: UiConstants.space3), - const UiShimmerLine(width: 140, height: 12), - const SizedBox(height: UiConstants.space2), - const UiShimmerLine(width: 200, height: 12), - ], - ), - ); - } -} +export 'shifts_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/index.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/index.dart new file mode 100644 index 00000000..1fffff3a --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/index.dart @@ -0,0 +1,2 @@ +export 'shift_card_skeleton.dart'; +export 'shifts_page_skeleton.dart'; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shift_card_skeleton.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shift_card_skeleton.dart new file mode 100644 index 00000000..db661acc --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shift_card_skeleton.dart @@ -0,0 +1,44 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Skeleton for a single shift card matching the shift list item layout. +/// +/// Shows a rounded container with placeholder lines for the shift title, +/// time, location, and a trailing status badge. +class ShiftCardSkeleton extends StatelessWidget { + /// Creates a [ShiftCardSkeleton]. + const ShiftCardSkeleton({super.key}); + + @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( + children: [ + const Expanded( + child: UiShimmerLine(width: 180, height: 16), + ), + const SizedBox(width: UiConstants.space3), + UiShimmerBox( + width: 64, + height: 24, + borderRadius: UiConstants.radiusFull, + ), + ], + ), + const SizedBox(height: UiConstants.space3), + const UiShimmerLine(width: 140, height: 12), + const SizedBox(height: UiConstants.space2), + const UiShimmerLine(width: 200, height: 12), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shifts_page_skeleton.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shifts_page_skeleton.dart new file mode 100644 index 00000000..844e8cf1 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shifts_page_skeleton/shifts_page_skeleton.dart @@ -0,0 +1,33 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'shift_card_skeleton.dart'; + +/// Shimmer loading skeleton for the shifts page body content. +/// +/// Mimics the loaded layout with a section header and a list of shift card +/// placeholders. Used while the initial shifts data is being fetched. +class ShiftsPageSkeleton extends StatelessWidget { + /// Creates a [ShiftsPageSkeleton]. + const ShiftsPageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 5, + itemBuilder: (index) => const ShiftCardSkeleton(), + ), + ], + ), + ), + ); + } +}