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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export 'billing_page_skeleton.dart';
export 'breakdown_row_skeleton.dart';
export 'invoice_card_skeleton.dart';

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export 'coverage_page_skeleton.dart';
export 'shift_card_skeleton.dart';

View File

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

View File

@@ -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();

View File

@@ -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 <Widget>[
// 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: <Widget>[
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space2),
SizedBox(
height: 164,
child: Row(
children: <Widget>[
_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: <Widget>[
const Row(
children: <Widget>[
UiShimmerBox(width: 36, height: 36),
SizedBox(width: UiConstants.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 100, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 80, height: 10),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
UiShimmerLine(width: 40, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 60, height: 10),
],
),
],
),
const SizedBox(height: UiConstants.space3),
const Row(
children: <Widget>[
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: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
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: <Widget>[
Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
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: <Widget>[
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';

View File

@@ -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: <Widget>[
UiShimmerBox(width: 36, height: 36),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 60, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 100, height: 10),
],
),
);
}
}

View File

@@ -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: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
Expanded(child: ActionCardSkeleton()),
SizedBox(width: UiConstants.space4),
Expanded(child: ActionCardSkeleton()),
],
),
],
);
}
}

View File

@@ -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 <Widget>[
// 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),
],
),
);
}
}

View File

@@ -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: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
Expanded(child: MetricCardSkeleton()),
SizedBox(width: UiConstants.space2),
Expanded(child: MetricCardSkeleton()),
SizedBox(width: UiConstants.space2),
Expanded(child: MetricCardSkeleton()),
],
),
],
);
}
}

View File

@@ -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: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
UiShimmerStatsCard(),
SizedBox(height: UiConstants.space3),
UiShimmerListItem(),
UiShimmerListItem(),
],
);
}
}

View File

@@ -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: <Widget>[
Row(
children: <Widget>[
UiShimmerCircle(size: 14),
SizedBox(width: UiConstants.space1),
UiShimmerLine(width: 40, height: 10),
],
),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 32, height: 20),
],
),
);
}
}

View File

@@ -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: <Widget>[
const Row(
children: <Widget>[
UiShimmerBox(width: 36, height: 36),
SizedBox(width: UiConstants.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 100, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 80, height: 10),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
UiShimmerLine(width: 40, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 60, height: 10),
],
),
],
),
const SizedBox(height: UiConstants.space3),
const Row(
children: <Widget>[
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,
),
],
),
);
}
}

View File

@@ -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 <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
SizedBox(
height: 164,
child: Row(
children: <Widget>[
ReorderCardSkeleton(),
SizedBox(width: UiConstants.space3),
ReorderCardSkeleton(),
],
),
),
],
);
}
}

View File

@@ -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: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
UiShimmerLine(width: 60, height: 10),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 70, height: 18),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 50, height: 10),
],
),
),
],
),
);
}
}

View File

@@ -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: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
SpendingCardSkeleton(),
],
);
}
}

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

View File

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

View File

@@ -0,0 +1,2 @@
export 'metric_card_skeleton.dart';
export 'metrics_grid_skeleton.dart';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export 'payment_item_skeleton.dart';
export 'payments_page_skeleton.dart';

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export 'shift_details_page_skeleton.dart';
export 'stat_card_skeleton.dart';

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export 'shift_card_skeleton.dart';
export 'shifts_page_skeleton.dart';

View File

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

View File

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