feat: add shimmer skeletons for various sections in the staff profile and onboarding features
- Implemented ProfilePageSkeleton for loading state in staff profile. - Added ReliabilityScoreSkeleton and ReliabilityStatsSkeleton for reliability metrics. - Created CertificatesSkeleton and related components for loading certificates. - Developed DocumentsSkeleton and associated document card skeletons. - Introduced TaxFormsSkeleton for loading tax forms. - Added BankAccountSkeleton and its components for bank account loading state. - Created TimeCardSkeleton for displaying time card loading state. - Implemented AttireSkeleton for loading attire items. - Added PersonalInfoSkeleton for loading personal information. - Developed FaqsSkeleton for loading FAQ sections. - Created PrivacySecuritySkeleton for loading privacy settings.
This commit is contained in:
@@ -35,6 +35,19 @@
|
|||||||
- Reports barrel: `widgets/reports_page/index.dart`
|
- Reports barrel: `widgets/reports_page/index.dart`
|
||||||
- Hubs: `packages/features/client/hubs/` (client_hubs_page + hub_details_page + edit_hub_page)
|
- Hubs: `packages/features/client/hubs/` (client_hubs_page + hub_details_page + edit_hub_page)
|
||||||
|
|
||||||
|
## Staff Profile Sections (shimmer done)
|
||||||
|
- Compliance: certificates, documents, tax_forms -- all have shimmer skeletons
|
||||||
|
- Finances: staff_bank_account, time_card -- all have shimmer skeletons
|
||||||
|
- Onboarding: attire, profile_info (personal_info_page only) -- have shimmer skeletons
|
||||||
|
- Support: faqs, privacy_security (including legal sub-pages) -- have shimmer skeletons
|
||||||
|
- Pages that intentionally keep CircularProgressIndicator (action/submit spinners):
|
||||||
|
- form_i9_page, form_w4_page (submit button spinners)
|
||||||
|
- experience_page (save button spinner)
|
||||||
|
- preferred_locations_page (save button + overlay spinner)
|
||||||
|
- certificate_upload_page, document_upload_page, attire_capture_page (form/upload pages, no initial load)
|
||||||
|
- language_selection_page (no loading state, static list)
|
||||||
|
- LegalDocumentSkeleton is shared between PrivacyPolicyPage and TermsOfServicePage
|
||||||
|
|
||||||
## Key Patterns Observed
|
## Key Patterns Observed
|
||||||
- BenefitsOverviewPage also has CircularProgressIndicator (not shimmer-ified yet)
|
- BenefitsOverviewPage also has CircularProgressIndicator (not shimmer-ified yet)
|
||||||
- ShiftDetailsPage has a dialog-level spinner in the "applying" dialog -- this is intentional, not a page loading state
|
- ShiftDetailsPage has a dialog-level spinner in the "applying" dialog -- this is intentional, not a page loading state
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -187,6 +187,9 @@ krow-workforce-export-latest/
|
|||||||
apps/mobile/packages/data_connect/lib/src/dataconnect_generated/
|
apps/mobile/packages/data_connect/lib/src/dataconnect_generated/
|
||||||
apps/web/src/dataconnect-generated/
|
apps/web/src/dataconnect-generated/
|
||||||
|
|
||||||
|
# Legacy mobile applications
|
||||||
|
apps/mobile/legacy/*
|
||||||
|
|
||||||
|
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
TASKS.md
|
TASKS.md
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../blocs/availability_bloc.dart';
|
import '../blocs/availability_bloc.dart';
|
||||||
import '../blocs/availability_event.dart';
|
import '../blocs/availability_event.dart';
|
||||||
import '../blocs/availability_state.dart';
|
import '../blocs/availability_state.dart';
|
||||||
|
import '../widgets/availability_page_skeleton/availability_page_skeleton.dart';
|
||||||
|
|
||||||
class AvailabilityPage extends StatefulWidget {
|
class AvailabilityPage extends StatefulWidget {
|
||||||
const AvailabilityPage({super.key});
|
const AvailabilityPage({super.key});
|
||||||
@@ -72,7 +73,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
|||||||
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
|
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is AvailabilityLoading) {
|
if (state is AvailabilityLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const AvailabilityPageSkeleton();
|
||||||
} else if (state is AvailabilityLoaded) {
|
} else if (state is AvailabilityLoaded) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export 'availability_page_skeleton/index.dart';
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'day_availability_skeleton.dart';
|
||||||
|
import 'info_card_skeleton.dart';
|
||||||
|
import 'quick_set_skeleton.dart';
|
||||||
|
import 'week_navigation_skeleton.dart';
|
||||||
|
|
||||||
|
/// Shimmer loading skeleton for the availability page.
|
||||||
|
///
|
||||||
|
/// Mimics the loaded layout: quick-set buttons, week navigation calendar,
|
||||||
|
/// selected day detail with time-slot rows, and an info card.
|
||||||
|
class AvailabilityPageSkeleton extends StatelessWidget {
|
||||||
|
/// Creates an [AvailabilityPageSkeleton].
|
||||||
|
const AvailabilityPageSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
spacing: UiConstants.space6,
|
||||||
|
children: const [
|
||||||
|
QuickSetSkeleton(),
|
||||||
|
WeekNavigationSkeleton(),
|
||||||
|
DayAvailabilitySkeleton(),
|
||||||
|
InfoCardSkeleton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the selected day detail card (header + time slot rows).
|
||||||
|
class DayAvailabilitySkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [DayAvailabilitySkeleton].
|
||||||
|
const DayAvailabilitySkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header: date text + toggle placeholder
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: const [
|
||||||
|
UiShimmerLine(width: 160, height: 16),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 80, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
UiShimmerBox(
|
||||||
|
width: 48,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
// 3 time-slot rows (morning, afternoon, evening)
|
||||||
|
..._buildSlotPlaceholders(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates 3 time-slot shimmer rows.
|
||||||
|
List<Widget> _buildSlotPlaceholders() {
|
||||||
|
return List.generate(3, (index) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: index < 2 ? UiConstants.space3 : 0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Icon placeholder
|
||||||
|
UiShimmerBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius:
|
||||||
|
UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
// Text lines
|
||||||
|
const Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
UiShimmerLine(width: 80, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 120, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Checkbox circle
|
||||||
|
const UiShimmerCircle(size: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export 'availability_page_skeleton.dart';
|
||||||
|
export 'day_availability_skeleton.dart';
|
||||||
|
export 'info_card_skeleton.dart';
|
||||||
|
export 'quick_set_skeleton.dart';
|
||||||
|
export 'week_navigation_skeleton.dart';
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the info card at the bottom (icon + two text lines).
|
||||||
|
class InfoCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates an [InfoCardSkeleton].
|
||||||
|
const InfoCardSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const UiShimmerCircle(size: 20),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: const [
|
||||||
|
UiShimmerLine(width: 140, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the quick-set section (title + 4 action buttons).
|
||||||
|
class QuickSetSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [QuickSetSkeleton].
|
||||||
|
const QuickSetSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Title line
|
||||||
|
const UiShimmerLine(width: 100, height: 14),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
// Row of 4 button placeholders
|
||||||
|
Row(
|
||||||
|
children: List.generate(4, (index) {
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: index == 0 ? 0 : UiConstants.space1,
|
||||||
|
right: index == 3 ? 0 : UiConstants.space1,
|
||||||
|
),
|
||||||
|
child: UiShimmerBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the week navigation card (month header + 7 day cells).
|
||||||
|
class WeekNavigationSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [WeekNavigationSkeleton].
|
||||||
|
const WeekNavigationSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Navigation header: left arrow, month label, right arrow
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const UiShimmerCircle(size: 32),
|
||||||
|
UiShimmerLine(width: 140, height: 16),
|
||||||
|
const UiShimmerCircle(size: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 7 day cells
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: List.generate(7, (_) {
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space1),
|
||||||
|
child: UiShimmerBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../bloc/clock_in_bloc.dart';
|
import '../bloc/clock_in_bloc.dart';
|
||||||
import '../bloc/clock_in_event.dart';
|
import '../bloc/clock_in_event.dart';
|
||||||
import '../bloc/clock_in_state.dart';
|
import '../bloc/clock_in_state.dart';
|
||||||
|
import '../widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart';
|
||||||
import '../widgets/commute_tracker.dart';
|
import '../widgets/commute_tracker.dart';
|
||||||
import '../widgets/date_selector.dart';
|
import '../widgets/date_selector.dart';
|
||||||
import '../widgets/lunch_break_modal.dart';
|
import '../widgets/lunch_break_modal.dart';
|
||||||
@@ -52,8 +53,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
builder: (BuildContext context, ClockInState state) {
|
builder: (BuildContext context, ClockInState state) {
|
||||||
if (state.status == ClockInStatus.loading &&
|
if (state.status == ClockInStatus.loading &&
|
||||||
state.todayShifts.isEmpty) {
|
state.todayShifts.isEmpty) {
|
||||||
return const Scaffold(
|
return Scaffold(
|
||||||
body: Center(child: CircularProgressIndicator()),
|
appBar: UiAppBar(title: i18n.title, showBackButton: false),
|
||||||
|
body: const SafeArea(child: ClockInPageSkeleton()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the "Your Activity" section header text.
|
||||||
|
class ActivityHeaderSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a shimmer line matching the activity header.
|
||||||
|
const ActivityHeaderSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: UiShimmerLine(width: 120, height: 18),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'activity_header_skeleton.dart';
|
||||||
|
import 'date_selector_skeleton.dart';
|
||||||
|
import 'shift_card_skeleton.dart';
|
||||||
|
import 'swipe_action_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while clock-in data loads.
|
||||||
|
///
|
||||||
|
/// Mirrors the loaded [ClockInPage] layout: date selector, activity header,
|
||||||
|
/// two shift cards, and the swipe-to-check-in bar.
|
||||||
|
class ClockInPageSkeleton extends StatelessWidget {
|
||||||
|
/// Creates the clock-in page shimmer skeleton.
|
||||||
|
const ClockInPageSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: UiConstants.space24,
|
||||||
|
top: UiConstants.space6,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: const <Widget>[
|
||||||
|
// Date selector row
|
||||||
|
DateSelectorSkeleton(),
|
||||||
|
SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
|
// "Your Activity" header
|
||||||
|
ActivityHeaderSkeleton(),
|
||||||
|
SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Shift cards (show two placeholders)
|
||||||
|
ShiftCardSkeleton(),
|
||||||
|
ShiftCardSkeleton(),
|
||||||
|
|
||||||
|
// Swipe action bar
|
||||||
|
SwipeActionSkeleton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the [DateSelector] row of 7 day chips.
|
||||||
|
class DateSelectorSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a shimmer placeholder matching the date selector layout.
|
||||||
|
const DateSelectorSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: List<Widget>.generate(7, (int index) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space1,
|
||||||
|
),
|
||||||
|
child: const UiShimmerBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single shift info card.
|
||||||
|
///
|
||||||
|
/// Mirrors the two-column layout: left side has badge, title, and subtitle
|
||||||
|
/// lines; right side has time range and rate lines.
|
||||||
|
class ShiftCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a shimmer placeholder for one shift card.
|
||||||
|
const ShiftCardSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Left column: badge + title + subtitle
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 80, height: 10),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 160, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 200, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
// Right column: time + rate
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 100, height: 12),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 60, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the swipe-to-check-in action area.
|
||||||
|
class SwipeActionSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a shimmer placeholder matching the swipe bar height.
|
||||||
|
const SwipeActionSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const UiShimmerBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
|||||||
import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/full_width_divider.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/full_width_divider.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
||||||
|
import 'package:staff_home/src/presentation/widgets/home_page/staff_home_header_skeleton.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/home_page_skeleton.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/home_page_skeleton.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/quick_actions_section.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/quick_actions_section.dart';
|
||||||
@@ -48,8 +49,13 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
BlocBuilder<HomeCubit, HomeState>(
|
BlocBuilder<HomeCubit, HomeState>(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.staffName != current.staffName,
|
previous.staffName != current.staffName ||
|
||||||
|
previous.status != current.status,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
if (state.status == HomeStatus.initial ||
|
||||||
|
state.status == HomeStatus.loading) {
|
||||||
|
return const StaffHomeHeaderSkeleton();
|
||||||
|
}
|
||||||
return HomeHeader(userName: state.staffName);
|
return HomeHeader(userName: state.staffName);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the staff home header during loading.
|
||||||
|
///
|
||||||
|
/// Mimics the avatar circle, welcome text, and user name layout.
|
||||||
|
class StaffHomeHeaderSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [StaffHomeHeaderSkeleton].
|
||||||
|
const StaffHomeHeaderSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space4,
|
||||||
|
UiConstants.space4,
|
||||||
|
UiConstants.space4,
|
||||||
|
UiConstants.space3,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
children: const <Widget>[
|
||||||
|
UiShimmerCircle(size: UiConstants.space12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 80, height: 12),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 120, height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import '../blocs/profile_cubit.dart';
|
|||||||
import '../blocs/profile_state.dart';
|
import '../blocs/profile_state.dart';
|
||||||
import '../widgets/logout_button.dart';
|
import '../widgets/logout_button.dart';
|
||||||
import '../widgets/header/profile_header.dart';
|
import '../widgets/header/profile_header.dart';
|
||||||
|
import '../widgets/profile_page_skeleton/profile_page_skeleton.dart';
|
||||||
import '../widgets/reliability_score_bar.dart';
|
import '../widgets/reliability_score_bar.dart';
|
||||||
import '../widgets/reliability_stats_card.dart';
|
import '../widgets/reliability_stats_card.dart';
|
||||||
import '../widgets/sections/index.dart';
|
import '../widgets/sections/index.dart';
|
||||||
@@ -63,9 +64,9 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (BuildContext context, ProfileState state) {
|
builder: (BuildContext context, ProfileState state) {
|
||||||
// Show loading spinner if status is loading
|
// Show shimmer skeleton while profile data loads
|
||||||
if (state.status == ProfileStatus.loading) {
|
if (state.status == ProfileStatus.loading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const ProfilePageSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == ProfileStatus.error) {
|
if (state.status == ProfileStatus.error) {
|
||||||
@@ -87,7 +88,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
|
|
||||||
final Staff? profile = state.profile;
|
final Staff? profile = state.profile;
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const ProfilePageSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export 'menu_section_skeleton.dart';
|
||||||
|
export 'profile_header_skeleton.dart';
|
||||||
|
export 'profile_page_skeleton.dart';
|
||||||
|
export 'reliability_score_skeleton.dart';
|
||||||
|
export 'reliability_stats_skeleton.dart';
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a profile menu section.
|
||||||
|
///
|
||||||
|
/// Mirrors the section layout: a section title line followed by a grid of
|
||||||
|
/// square menu item placeholders. Reused for onboarding, compliance, finance,
|
||||||
|
/// and support sections.
|
||||||
|
class MenuSectionSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [MenuSectionSkeleton].
|
||||||
|
const MenuSectionSkeleton({
|
||||||
|
super.key,
|
||||||
|
this.itemCount = 4,
|
||||||
|
this.crossAxisCount = 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Number of menu item placeholders to display.
|
||||||
|
final int itemCount;
|
||||||
|
|
||||||
|
/// Number of columns in the grid.
|
||||||
|
final int crossAxisCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Section title placeholder
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: UiConstants.space1),
|
||||||
|
child: const UiShimmerLine(width: 100, height: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
// Menu items grid
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
const double spacing = UiConstants.space3;
|
||||||
|
final double totalWidth = constraints.maxWidth;
|
||||||
|
final double totalSpacingWidth = spacing * (crossAxisCount - 1);
|
||||||
|
final double itemWidth =
|
||||||
|
(totalWidth - totalSpacingWidth) / crossAxisCount;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: spacing,
|
||||||
|
children: List<Widget>.generate(itemCount, (int index) {
|
||||||
|
return SizedBox(
|
||||||
|
width: itemWidth,
|
||||||
|
child: const _MenuItemSkeleton(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single menu item shimmer: a bordered square with an icon circle and label
|
||||||
|
/// line.
|
||||||
|
class _MenuItemSkeleton extends StatelessWidget {
|
||||||
|
const _MenuItemSkeleton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgPopup,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space2),
|
||||||
|
child: const AspectRatio(
|
||||||
|
aspectRatio: 1.0,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerBox(width: 36, height: 36),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 48, height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the profile header section.
|
||||||
|
///
|
||||||
|
/// Mirrors [ProfileHeader] layout: circle avatar, name line, and level badge
|
||||||
|
/// on the primary-colored background with rounded bottom corners.
|
||||||
|
class ProfileHeaderSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [ProfileHeaderSkeleton].
|
||||||
|
const ProfileHeaderSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space16,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
bottom: Radius.circular(UiConstants.space6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// Avatar placeholder
|
||||||
|
const UiShimmerCircle(size: 112),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
// Name placeholder
|
||||||
|
const UiShimmerLine(width: 160, height: 20),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
// Level badge placeholder
|
||||||
|
const UiShimmerBox(width: 100, height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'menu_section_skeleton.dart';
|
||||||
|
import 'profile_header_skeleton.dart';
|
||||||
|
import 'reliability_score_skeleton.dart';
|
||||||
|
import 'reliability_stats_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton for [StaffProfilePage].
|
||||||
|
///
|
||||||
|
/// Mimics the loaded profile layout: header, reliability stats, score bar,
|
||||||
|
/// and four menu sections. Displayed while [ProfileCubit] fetches data.
|
||||||
|
class ProfilePageSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [ProfilePageSkeleton].
|
||||||
|
const ProfilePageSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// Header with avatar, name, and badge
|
||||||
|
const ProfileHeaderSkeleton(),
|
||||||
|
|
||||||
|
// Content offset to overlap the header bottom radius
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(0, -UiConstants.space6),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
spacing: UiConstants.space6,
|
||||||
|
children: const <Widget>[
|
||||||
|
// Reliability stats row (5 items)
|
||||||
|
ReliabilityStatsSkeleton(),
|
||||||
|
|
||||||
|
// Reliability score bar
|
||||||
|
ReliabilityScoreSkeleton(),
|
||||||
|
|
||||||
|
// Onboarding section (4 items, 3 columns)
|
||||||
|
MenuSectionSkeleton(itemCount: 4, crossAxisCount: 3),
|
||||||
|
|
||||||
|
// Compliance section (3 items, 3 columns)
|
||||||
|
MenuSectionSkeleton(itemCount: 3, crossAxisCount: 3),
|
||||||
|
|
||||||
|
// Finance section (3 items, 3 columns)
|
||||||
|
MenuSectionSkeleton(itemCount: 3, crossAxisCount: 3),
|
||||||
|
|
||||||
|
// Support section (2 items, 3 columns)
|
||||||
|
MenuSectionSkeleton(itemCount: 2, crossAxisCount: 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the reliability score bar section.
|
||||||
|
///
|
||||||
|
/// Mirrors [ReliabilityScoreBar] layout: a tinted container with a title line,
|
||||||
|
/// percentage line, progress bar placeholder, and description line.
|
||||||
|
class ReliabilityScoreSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [ReliabilityScoreSkeleton].
|
||||||
|
const ReliabilityScoreSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withValues(alpha: 0.1),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Title row with label and percentage
|
||||||
|
const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 120, height: 14),
|
||||||
|
UiShimmerLine(width: 40, height: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
// Progress bar placeholder
|
||||||
|
const UiShimmerBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
// Description line
|
||||||
|
const UiShimmerLine(width: 200, height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the reliability stats card.
|
||||||
|
///
|
||||||
|
/// Mirrors [ReliabilityStatsCard] layout: a bordered card containing five
|
||||||
|
/// evenly-spaced stat columns, each with an icon circle, value line, and
|
||||||
|
/// label line.
|
||||||
|
class ReliabilityStatsSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [ReliabilityStatsSkeleton].
|
||||||
|
const ReliabilityStatsSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgPopup,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
_StatItemSkeleton(),
|
||||||
|
_StatItemSkeleton(),
|
||||||
|
_StatItemSkeleton(),
|
||||||
|
_StatItemSkeleton(),
|
||||||
|
_StatItemSkeleton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single stat column shimmer: icon circle, value line, label line.
|
||||||
|
class _StatItemSkeleton extends StatelessWidget {
|
||||||
|
const _StatItemSkeleton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerBox(width: UiConstants.space10, height: UiConstants.space10),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 28, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 36, height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import '../blocs/certificates/certificates_state.dart';
|
|||||||
import '../widgets/add_certificate_card.dart';
|
import '../widgets/add_certificate_card.dart';
|
||||||
import '../widgets/certificate_card.dart';
|
import '../widgets/certificate_card.dart';
|
||||||
import '../widgets/certificates_header.dart';
|
import '../widgets/certificates_header.dart';
|
||||||
|
import '../widgets/certificates_skeleton/certificates_skeleton.dart';
|
||||||
|
|
||||||
/// Page for viewing and managing staff certificates.
|
/// Page for viewing and managing staff certificates.
|
||||||
///
|
///
|
||||||
@@ -28,9 +29,7 @@ class CertificatesPage extends StatelessWidget {
|
|||||||
builder: (BuildContext context, CertificatesState state) {
|
builder: (BuildContext context, CertificatesState state) {
|
||||||
if (state.status == CertificatesStatus.loading ||
|
if (state.status == CertificatesStatus.loading ||
|
||||||
state.status == CertificatesStatus.initial) {
|
state.status == CertificatesStatus.initial) {
|
||||||
return const Scaffold(
|
return const Scaffold(body: CertificatesSkeleton());
|
||||||
body: Center(child: CircularProgressIndicator()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == CertificatesStatus.failure) {
|
if (state.status == CertificatesStatus.failure) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single certificate card.
|
||||||
|
class CertificateCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [CertificateCardSkeleton].
|
||||||
|
const CertificateCardSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerCircle(size: 40),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 140, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 100, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiShimmerBox(width: 60, height: 28),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the certificates progress header.
|
||||||
|
class CertificatesHeaderSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [CertificatesHeaderSkeleton].
|
||||||
|
const CertificatesHeaderSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
decoration: const BoxDecoration(color: UiColors.primary),
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
const UiShimmerCircle(size: 64),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
UiShimmerLine(
|
||||||
|
width: 120,
|
||||||
|
height: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(
|
||||||
|
width: 80,
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'certificate_card_skeleton.dart';
|
||||||
|
import 'certificates_header_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while certificates are loading.
|
||||||
|
class CertificatesSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [CertificatesSkeleton].
|
||||||
|
const CertificatesSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const CertificatesHeaderSkeleton(),
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(0, -UiConstants.space12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
|
child: UiShimmerList(
|
||||||
|
itemCount: 4,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
itemBuilder: (int index) =>
|
||||||
|
const CertificateCardSkeleton(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import '../blocs/documents/documents_cubit.dart';
|
|||||||
import '../blocs/documents/documents_state.dart';
|
import '../blocs/documents/documents_state.dart';
|
||||||
import '../widgets/document_card.dart';
|
import '../widgets/document_card.dart';
|
||||||
import '../widgets/documents_progress_card.dart';
|
import '../widgets/documents_progress_card.dart';
|
||||||
|
import '../widgets/documents_skeleton/documents_skeleton.dart';
|
||||||
|
|
||||||
class DocumentsPage extends StatelessWidget {
|
class DocumentsPage extends StatelessWidget {
|
||||||
const DocumentsPage({super.key});
|
const DocumentsPage({super.key});
|
||||||
@@ -28,11 +29,7 @@ class DocumentsPage extends StatelessWidget {
|
|||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (BuildContext context, DocumentsState state) {
|
builder: (BuildContext context, DocumentsState state) {
|
||||||
if (state.status == DocumentsStatus.loading) {
|
if (state.status == DocumentsStatus.loading) {
|
||||||
return const Center(
|
return const DocumentsSkeleton();
|
||||||
child: CircularProgressIndicator(
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(UiColors.primary),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (state.status == DocumentsStatus.failure) {
|
if (state.status == DocumentsStatus.failure) {
|
||||||
return Center(
|
return Center(
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single document card row.
|
||||||
|
class DocumentCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [DocumentCardSkeleton].
|
||||||
|
const DocumentCardSkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerCircle(size: 40),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 160, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 100, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiShimmerBox(width: 24, height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the documents progress card.
|
||||||
|
class DocumentsProgressSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [DocumentsProgressSkeleton].
|
||||||
|
const DocumentsProgressSkeleton({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: const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 140, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space3),
|
||||||
|
UiShimmerBox(width: double.infinity, height: 8),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 80, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'document_card_skeleton.dart';
|
||||||
|
import 'documents_progress_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while documents are loading.
|
||||||
|
class DocumentsSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [DocumentsSkeleton].
|
||||||
|
const DocumentsSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
vertical: UiConstants.space6,
|
||||||
|
),
|
||||||
|
children: <Widget>[
|
||||||
|
const DocumentsProgressSkeleton(),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 5,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
itemBuilder: (int index) => const DocumentCardSkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../blocs/tax_forms/tax_forms_cubit.dart';
|
import '../blocs/tax_forms/tax_forms_cubit.dart';
|
||||||
import '../blocs/tax_forms/tax_forms_state.dart';
|
import '../blocs/tax_forms/tax_forms_state.dart';
|
||||||
import '../widgets/tax_forms_page/index.dart';
|
import '../widgets/tax_forms_page/index.dart';
|
||||||
|
import '../widgets/tax_forms_skeleton/tax_forms_skeleton.dart';
|
||||||
|
|
||||||
class TaxFormsPage extends StatelessWidget {
|
class TaxFormsPage extends StatelessWidget {
|
||||||
const TaxFormsPage({super.key});
|
const TaxFormsPage({super.key});
|
||||||
@@ -31,7 +32,7 @@ class TaxFormsPage extends StatelessWidget {
|
|||||||
child: BlocBuilder<TaxFormsCubit, TaxFormsState>(
|
child: BlocBuilder<TaxFormsCubit, TaxFormsState>(
|
||||||
builder: (BuildContext context, TaxFormsState state) {
|
builder: (BuildContext context, TaxFormsState state) {
|
||||||
if (state.status == TaxFormsStatus.loading) {
|
if (state.status == TaxFormsStatus.loading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const TaxFormsSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == TaxFormsStatus.failure) {
|
if (state.status == TaxFormsStatus.failure) {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single tax form card.
|
||||||
|
class TaxFormCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [TaxFormCardSkeleton].
|
||||||
|
const TaxFormCardSkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerCircle(size: 40),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 120, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 80, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiShimmerBox(width: 60, height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'tax_form_card_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while tax forms are loading.
|
||||||
|
class TaxFormsSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [TaxFormsSkeleton].
|
||||||
|
const TaxFormsSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
vertical: UiConstants.space6,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
children: <Widget>[
|
||||||
|
// Info card placeholder
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
color: UiColors.cardViewBackground,
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 180, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(height: 12),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 200, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Progress overview placeholder
|
||||||
|
const UiShimmerStatsCard(),
|
||||||
|
// Form card placeholders
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 3,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => const TaxFormCardSkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import '../blocs/bank_account_cubit.dart';
|
|||||||
import '../blocs/bank_account_state.dart';
|
import '../blocs/bank_account_state.dart';
|
||||||
import '../widgets/account_card.dart';
|
import '../widgets/account_card.dart';
|
||||||
import '../widgets/add_account_form.dart';
|
import '../widgets/add_account_form.dart';
|
||||||
|
import '../widgets/bank_account_skeleton/bank_account_skeleton.dart';
|
||||||
import '../widgets/security_notice.dart';
|
import '../widgets/security_notice.dart';
|
||||||
|
|
||||||
class BankAccountPage extends StatelessWidget {
|
class BankAccountPage extends StatelessWidget {
|
||||||
@@ -49,7 +50,7 @@ class BankAccountPage extends StatelessWidget {
|
|||||||
builder: (BuildContext context, BankAccountState state) {
|
builder: (BuildContext context, BankAccountState state) {
|
||||||
if (state.status == BankAccountStatus.loading &&
|
if (state.status == BankAccountStatus.loading &&
|
||||||
state.accounts.isEmpty) {
|
state.accounts.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const BankAccountSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == BankAccountStatus.error) {
|
if (state.status == BankAccountStatus.error) {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single bank account card.
|
||||||
|
class AccountCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates an [AccountCardSkeleton].
|
||||||
|
const AccountCardSkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerCircle(size: 40),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 140, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 100, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiShimmerBox(width: 48, height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'account_card_skeleton.dart';
|
||||||
|
import 'security_notice_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while bank accounts are loading.
|
||||||
|
class BankAccountSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [BankAccountSkeleton].
|
||||||
|
const BankAccountSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const SecurityNoticeSkeleton(),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 2,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
itemBuilder: (int index) => const AccountCardSkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the security notice banner.
|
||||||
|
class SecurityNoticeSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [SecurityNoticeSkeleton].
|
||||||
|
const SecurityNoticeSkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerCircle(size: 24),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 160, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:krow_core/core.dart';
|
|||||||
import '../blocs/time_card_bloc.dart';
|
import '../blocs/time_card_bloc.dart';
|
||||||
import '../widgets/month_selector.dart';
|
import '../widgets/month_selector.dart';
|
||||||
import '../widgets/shift_history_list.dart';
|
import '../widgets/shift_history_list.dart';
|
||||||
|
import '../widgets/time_card_skeleton/time_card_skeleton.dart';
|
||||||
import '../widgets/time_card_summary.dart';
|
import '../widgets/time_card_summary.dart';
|
||||||
|
|
||||||
/// The main page for displaying the staff time card.
|
/// The main page for displaying the staff time card.
|
||||||
@@ -50,7 +51,7 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
|||||||
},
|
},
|
||||||
builder: (BuildContext context, TimeCardState state) {
|
builder: (BuildContext context, TimeCardState state) {
|
||||||
if (state is TimeCardLoading) {
|
if (state is TimeCardLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const TimeCardSkeleton();
|
||||||
} else if (state is TimeCardError) {
|
} else if (state is TimeCardError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the month selector row.
|
||||||
|
class MonthSelectorSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [MonthSelectorSkeleton].
|
||||||
|
const MonthSelectorSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerCircle(size: 32),
|
||||||
|
UiShimmerLine(width: 120, height: 16),
|
||||||
|
UiShimmerCircle(size: 32),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single shift history row.
|
||||||
|
class ShiftHistorySkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [ShiftHistorySkeleton].
|
||||||
|
const ShiftHistorySkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 140, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 100, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 60, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 40, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'month_selector_skeleton.dart';
|
||||||
|
import 'shift_history_skeleton.dart';
|
||||||
|
import 'time_card_summary_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while time card data is loading.
|
||||||
|
class TimeCardSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [TimeCardSkeleton].
|
||||||
|
const TimeCardSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
vertical: UiConstants.space6,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const MonthSelectorSkeleton(),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
const TimeCardSummarySkeleton(),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 5,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
itemBuilder: (int index) => const ShiftHistorySkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the time card summary (hours + earnings).
|
||||||
|
class TimeCardSummarySkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [TimeCardSummarySkeleton].
|
||||||
|
const TimeCardSummarySkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(child: UiShimmerStatsCard()),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(child: UiShimmerStatsCard()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import '../widgets/attire_info_card.dart';
|
|||||||
import '../widgets/attire_item_card.dart';
|
import '../widgets/attire_item_card.dart';
|
||||||
import '../widgets/attire_section_header.dart';
|
import '../widgets/attire_section_header.dart';
|
||||||
import '../widgets/attire_section_tab.dart';
|
import '../widgets/attire_section_tab.dart';
|
||||||
|
import '../widgets/attire_skeleton/attire_skeleton.dart';
|
||||||
|
|
||||||
class AttirePage extends StatefulWidget {
|
class AttirePage extends StatefulWidget {
|
||||||
const AttirePage({super.key});
|
const AttirePage({super.key});
|
||||||
@@ -49,7 +50,7 @@ class _AttirePageState extends State<AttirePage> {
|
|||||||
},
|
},
|
||||||
builder: (BuildContext context, AttireState state) {
|
builder: (BuildContext context, AttireState state) {
|
||||||
if (state.status == AttireStatus.loading && state.options.isEmpty) {
|
if (state.status == AttireStatus.loading && state.options.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const AttireSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<AttireItem> requiredItems = state.options
|
final List<AttireItem> requiredItems = state.options
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single attire item card.
|
||||||
|
class AttireItemSkeleton extends StatelessWidget {
|
||||||
|
/// Creates an [AttireItemSkeleton].
|
||||||
|
const AttireItemSkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerBox(width: 56, height: 56),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 120, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(width: 80, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiShimmerBox(width: 60, height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'attire_item_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while attire items are loading.
|
||||||
|
class AttireSkeleton extends StatelessWidget {
|
||||||
|
/// Creates an [AttireSkeleton].
|
||||||
|
const AttireSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Info card placeholder
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
color: UiColors.cardViewBackground,
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 160, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerLine(height: 12),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 200, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
// Section toggle chips placeholder
|
||||||
|
const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerBox(width: 80, height: 32),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
UiShimmerBox(width: 100, height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
// Section header placeholder
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
// Attire item cards
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 4,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
itemBuilder: (int index) => const AttireItemSkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:krow_core/core.dart';
|
|||||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
|
||||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
|
||||||
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_content.dart';
|
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_content.dart';
|
||||||
|
import 'package:staff_profile_info/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart';
|
||||||
|
|
||||||
|
|
||||||
/// The Personal Info page for staff onboarding.
|
/// The Personal Info page for staff onboarding.
|
||||||
@@ -56,7 +57,7 @@ class PersonalInfoPage extends StatelessWidget {
|
|||||||
builder: (BuildContext context, PersonalInfoState state) {
|
builder: (BuildContext context, PersonalInfoState state) {
|
||||||
if (state.status == PersonalInfoStatus.loading ||
|
if (state.status == PersonalInfoStatus.loading ||
|
||||||
state.status == PersonalInfoStatus.initial) {
|
state.status == PersonalInfoStatus.initial) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const PersonalInfoSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.staff == null) {
|
if (state.staff == null) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single form field (label + input).
|
||||||
|
class FormFieldSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [FormFieldSkeleton].
|
||||||
|
const FormFieldSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 80, height: 12),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
UiShimmerBox(width: double.infinity, height: 48),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'form_field_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while personal info is loading.
|
||||||
|
class PersonalInfoSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [PersonalInfoSkeleton].
|
||||||
|
const PersonalInfoSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// Avatar placeholder
|
||||||
|
const Center(child: UiShimmerCircle(size: 80)),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
// Form fields
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 5,
|
||||||
|
spacing: UiConstants.space5,
|
||||||
|
itemBuilder: (int index) => const FormFieldSkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single FAQ accordion item.
|
||||||
|
class FaqItemSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [FaqItemSkeleton].
|
||||||
|
const FaqItemSkeleton({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: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: UiShimmerLine(height: 14),
|
||||||
|
),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
UiShimmerCircle(size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'faq_item_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while FAQs are loading.
|
||||||
|
class FaqsSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [FaqsSkeleton].
|
||||||
|
const FaqsSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space24,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Search bar placeholder
|
||||||
|
UiShimmerBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
// Category header
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
// FAQ items
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 3,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => const FaqItemSkeleton(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
// Second category
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 3,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => const FaqItemSkeleton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:staff_faqs/src/presentation/blocs/faqs_bloc.dart';
|
import 'package:staff_faqs/src/presentation/blocs/faqs_bloc.dart';
|
||||||
|
import 'faqs_skeleton/faqs_skeleton.dart';
|
||||||
|
|
||||||
/// Widget displaying FAQs with search functionality and accordion items
|
/// Widget displaying FAQs with search functionality and accordion items
|
||||||
class FaqsWidget extends StatefulWidget {
|
class FaqsWidget extends StatefulWidget {
|
||||||
@@ -76,10 +77,7 @@ class _FaqsWidgetState extends State<FaqsWidget> {
|
|||||||
|
|
||||||
// FAQ List or Empty State
|
// FAQ List or Empty State
|
||||||
if (state.isLoading)
|
if (state.isLoading)
|
||||||
const Padding(
|
const FaqsSkeleton()
|
||||||
padding: EdgeInsets.symmetric(vertical: 48),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
else if (state.categories.isEmpty)
|
else if (state.categories.isEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
import '../../blocs/legal/privacy_policy_cubit.dart';
|
import '../../blocs/legal/privacy_policy_cubit.dart';
|
||||||
|
import '../../widgets/skeletons/legal_document_skeleton.dart';
|
||||||
|
|
||||||
/// Page displaying the Privacy Policy document
|
/// Page displaying the Privacy Policy document
|
||||||
class PrivacyPolicyPage extends StatelessWidget {
|
class PrivacyPolicyPage extends StatelessWidget {
|
||||||
@@ -24,9 +25,7 @@ class PrivacyPolicyPage extends StatelessWidget {
|
|||||||
child: BlocBuilder<PrivacyPolicyCubit, PrivacyPolicyState>(
|
child: BlocBuilder<PrivacyPolicyCubit, PrivacyPolicyState>(
|
||||||
builder: (BuildContext context, PrivacyPolicyState state) {
|
builder: (BuildContext context, PrivacyPolicyState state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(
|
return const LegalDocumentSkeleton();
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.error != null) {
|
if (state.error != null) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
import '../../blocs/legal/terms_cubit.dart';
|
import '../../blocs/legal/terms_cubit.dart';
|
||||||
|
import '../../widgets/skeletons/legal_document_skeleton.dart';
|
||||||
|
|
||||||
/// Page displaying the Terms of Service document
|
/// Page displaying the Terms of Service document
|
||||||
class TermsOfServicePage extends StatelessWidget {
|
class TermsOfServicePage extends StatelessWidget {
|
||||||
@@ -24,9 +25,7 @@ class TermsOfServicePage extends StatelessWidget {
|
|||||||
child: BlocBuilder<TermsCubit, TermsState>(
|
child: BlocBuilder<TermsCubit, TermsState>(
|
||||||
builder: (BuildContext context, TermsState state) {
|
builder: (BuildContext context, TermsState state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(
|
return const LegalDocumentSkeleton();
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.error != null) {
|
if (state.error != null) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import '../blocs/privacy_security_bloc.dart';
|
import '../blocs/privacy_security_bloc.dart';
|
||||||
import '../widgets/legal/legal_section_widget.dart';
|
import '../widgets/legal/legal_section_widget.dart';
|
||||||
import '../widgets/privacy/privacy_section_widget.dart';
|
import '../widgets/privacy/privacy_section_widget.dart';
|
||||||
|
import '../widgets/skeletons/privacy_security_skeleton.dart';
|
||||||
|
|
||||||
/// Page displaying privacy & security settings for staff
|
/// Page displaying privacy & security settings for staff
|
||||||
class PrivacySecurityPage extends StatelessWidget {
|
class PrivacySecurityPage extends StatelessWidget {
|
||||||
@@ -25,7 +26,7 @@ class PrivacySecurityPage extends StatelessWidget {
|
|||||||
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
|
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
|
||||||
builder: (BuildContext context, PrivacySecurityState state) {
|
builder: (BuildContext context, PrivacySecurityState state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const UiLoadingPage();
|
return const PrivacySecuritySkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
return const SingleChildScrollView(
|
return const SingleChildScrollView(
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shared shimmer skeleton for legal document pages (Privacy Policy, Terms).
|
||||||
|
///
|
||||||
|
/// Simulates a long-form text document with varied line widths.
|
||||||
|
class LegalDocumentSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [LegalDocumentSkeleton].
|
||||||
|
const LegalDocumentSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Title line
|
||||||
|
const UiShimmerLine(width: 200, height: 18),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
// Body text lines with varied widths
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 4,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => const UiShimmerLine(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
const UiShimmerLine(width: 180, height: 16),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 5,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => UiShimmerLine(
|
||||||
|
width: index == 4 ? 200 : double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
const UiShimmerLine(width: 160, height: 16),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 3,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => const UiShimmerLine(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
const UiShimmerLine(width: 140, height: 16),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 4,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
itemBuilder: (int index) => UiShimmerLine(
|
||||||
|
width: index == 3 ? 160 : double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'settings_toggle_skeleton.dart';
|
||||||
|
|
||||||
|
/// Full-page shimmer skeleton shown while privacy settings are loading.
|
||||||
|
class PrivacySecuritySkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [PrivacySecuritySkeleton].
|
||||||
|
const PrivacySecuritySkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiShimmer(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space6),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Privacy section header
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 3,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
itemBuilder: (int index) => const SettingsToggleSkeleton(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
// Legal section header
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
// Legal links
|
||||||
|
UiShimmerList(
|
||||||
|
itemCount: 2,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
itemBuilder: (int index) => const UiShimmerListItem(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single settings toggle row.
|
||||||
|
class SettingsToggleSkeleton extends StatelessWidget {
|
||||||
|
/// Creates a [SettingsToggleSkeleton].
|
||||||
|
const SettingsToggleSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerLine(width: 140, height: 14),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
UiShimmerLine(width: 200, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: UiConstants.space3),
|
||||||
|
UiShimmerBox(width: 48, height: 28),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user