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.
This commit is contained in:
Achintha Isuru
2026-03-10 14:25:56 -04:00
parent 2d6133aba8
commit 4423775fa1
52 changed files with 1603 additions and 1443 deletions

View File

@@ -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: <Widget>[
// Section header placeholder (dot + title + count)
const _SectionHeaderSkeleton(),
// Order card placeholders
...List<Widget>.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: <Widget>[
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: <Widget>[
// Status and type badges
Row(
children: <Widget>[
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: <Widget>[
UiShimmerCircle(size: 14),
SizedBox(width: UiConstants.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
_StatItemSkeleton(),
_StatDividerSkeleton(),
_StatItemSkeleton(),
_StatDividerSkeleton(),
_StatItemSkeleton(),
],
),
),
const SizedBox(height: UiConstants.space5),
// Time boxes (clock in / clock out)
Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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';

View File

@@ -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';

View File

@@ -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: <Widget>[
// Status and type badges
Row(
children: <Widget>[
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: <Widget>[
UiShimmerCircle(size: 14),
SizedBox(width: UiConstants.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
StatItemSkeleton(),
StatDividerSkeleton(),
StatItemSkeleton(),
StatDividerSkeleton(),
StatItemSkeleton(),
],
),
),
const SizedBox(height: UiConstants.space5),
// Time boxes (clock in / clock out)
Row(
children: <Widget>[
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: <Widget>[
UiShimmerLine(width: 60, height: 10),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 80, height: 16),
],
),
);
}
}

View File

@@ -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: <Widget>[
UiShimmerCircle(size: 8),
SizedBox(width: UiConstants.space2),
UiShimmerLine(width: 100, height: 14),
SizedBox(width: UiConstants.space1),
UiShimmerLine(width: 24, height: 14),
],
),
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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: <Widget>[
UiShimmerCircle(size: 14),
UiShimmerLine(width: 32, height: 16),
UiShimmerLine(width: 40, height: 10),
],
);
}
}

View File

@@ -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: <Widget>[
// Section header placeholder (dot + title + count)
const SectionHeaderSkeleton(),
// Order card placeholders
...List<Widget>.generate(3, (int index) {
return const Padding(
padding: EdgeInsets.only(bottom: UiConstants.space3),
child: OrderCardSkeleton(),
);
}),
],
),
);
}
}