feat: Add UiEmptyState widget and integrate it into BankAccountPage and WorkerHomePage for improved empty state handling

This commit is contained in:
Achintha Isuru
2026-03-01 03:22:48 -05:00
parent 015f1fbc1b
commit 2c61baaaa9
5 changed files with 240 additions and 183 deletions

View File

@@ -13,3 +13,4 @@ export 'src/widgets/ui_chip.dart';
export 'src/widgets/ui_loading_page.dart'; export 'src/widgets/ui_loading_page.dart';
export 'src/widgets/ui_snackbar.dart'; export 'src/widgets/ui_snackbar.dart';
export 'src/widgets/ui_notice_banner.dart'; export 'src/widgets/ui_notice_banner.dart';
export 'src/widgets/ui_empty_state.dart';

View File

@@ -131,6 +131,15 @@ class UiTypography {
color: UiColors.textPrimary, color: UiColors.textPrimary,
); );
/// Title 1 Bold - Font: Instrument Sans, Size: 16, Height: 1.5 (#121826)
/// Used for section headers and important labels.
static final TextStyle title1b = _primaryBase.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
height: 1.5,
color: UiColors.textPrimary,
);
/// Title 2 Bold - Font: Instrument Sans, Size: 20, Height: 1.1 (#121826) /// Title 2 Bold - Font: Instrument Sans, Size: 20, Height: 1.1 (#121826)
static final TextStyle title2b = _primaryBase.copyWith( static final TextStyle title2b = _primaryBase.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@@ -0,0 +1,44 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class UiEmptyState extends StatelessWidget {
const UiEmptyState({
super.key,
required this.icon,
required this.title,
required this.description,
this.iconColor,
});
final IconData icon;
final String title;
final String description;
final Color? iconColor;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(icon, size: 64, color: iconColor ?? UiColors.iconDisabled),
const SizedBox(height: UiConstants.space5),
Text(
title,
style: UiTypography.title1b.textDescription,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: Text(
description,
style: UiTypography.body2m.textDescription,
textAlign: TextAlign.center,
),
),
],
),
);
}
}

View File

@@ -61,12 +61,41 @@ class WorkerHomePage extends StatelessWidget {
horizontal: UiConstants.space4, horizontal: UiConstants.space4,
vertical: UiConstants.space4, vertical: UiConstants.space4,
), ),
child: Column( child: BlocBuilder<HomeCubit, HomeState>(
children: [ buildWhen: (previous, current) =>
BlocBuilder<HomeCubit, HomeState>( previous.isProfileComplete != current.isProfileComplete,
builder: (context, state) { builder: (context, state) {
if (state.isProfileComplete) return const SizedBox(); if (!state.isProfileComplete) {
return PlaceholderBanner( return SizedBox(
height: MediaQuery.of(context).size.height -
300,
child: Column(
children: [
PlaceholderBanner(
title: bannersI18n.complete_profile_title,
subtitle: bannersI18n.complete_profile_subtitle,
bg: UiColors.primaryInverse,
accent: UiColors.primary,
onTap: () {
Modular.to.toProfile();
},
),
const SizedBox(height: UiConstants.space10),
Expanded(
child: UiEmptyState(
icon: UiIcons.users,
title: 'Complete Your Profile',
description: 'Finish setting up your profile to unlock shifts, view earnings, and start earning today.',
),
),
],
),
);
}
return Column(
children: [
PlaceholderBanner(
title: bannersI18n.complete_profile_title, title: bannersI18n.complete_profile_title,
subtitle: bannersI18n.complete_profile_subtitle, subtitle: bannersI18n.complete_profile_subtitle,
bg: UiColors.primaryInverse, bg: UiColors.primaryInverse,
@@ -74,156 +103,156 @@ class WorkerHomePage extends StatelessWidget {
onTap: () { onTap: () {
Modular.to.toProfile(); Modular.to.toProfile();
}, },
);
},
),
const SizedBox(height: UiConstants.space6),
// Quick Actions
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: QuickActionItem(
icon: UiIcons.search,
label: quickI18n.find_shifts,
onTap: () => Modular.to.toShifts(),
),
), ),
Expanded(
child: QuickActionItem(
icon: UiIcons.calendar,
label: quickI18n.availability,
onTap: () => Modular.to.toAvailability(),
),
),
Expanded(
child: QuickActionItem(
icon: UiIcons.dollar,
label: quickI18n.earnings,
onTap: () => Modular.to.toPayments(),
),
),
],
),
const SizedBox(height: UiConstants.space6),
// Today's Shifts const SizedBox(height: UiConstants.space6),
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) { // Quick Actions
final shifts = state.todayShifts; Row(
return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SectionHeader( Expanded(
title: sectionsI18n.todays_shift, child: QuickActionItem(
action: shifts.isNotEmpty icon: UiIcons.search,
? sectionsI18n.scheduled_count( label: quickI18n.find_shifts,
count: shifts.length, onTap: () => Modular.to.toShifts(),
) ),
: null,
), ),
if (state.status == HomeStatus.loading) Expanded(
const Center( child: QuickActionItem(
child: SizedBox( icon: UiIcons.calendar,
height: UiConstants.space10, label: quickI18n.availability,
width: UiConstants.space10, onTap: () => Modular.to.toAvailability(),
child: CircularProgressIndicator( ),
color: UiColors.primary, ),
Expanded(
child: QuickActionItem(
icon: UiIcons.dollar,
label: quickI18n.earnings,
onTap: () => Modular.to.toPayments(),
),
),
],
),
const SizedBox(height: UiConstants.space6),
// Today's Shifts
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
final shifts = state.todayShifts;
return Column(
children: [
SectionHeader(
title: sectionsI18n.todays_shift,
action: shifts.isNotEmpty
? sectionsI18n.scheduled_count(
count: shifts.length,
)
: null,
),
if (state.status == HomeStatus.loading)
const Center(
child: SizedBox(
height: UiConstants.space10,
width: UiConstants.space10,
child: CircularProgressIndicator(
color: UiColors.primary,
),
),
)
else if (shifts.isEmpty)
EmptyStateWidget(
message: emptyI18n.no_shifts_today,
actionLink: emptyI18n.find_shifts_cta,
onAction: () =>
Modular.to.toShifts(initialTab: 'find'),
)
else
Column(
children: shifts
.map(
(shift) => ShiftCard(
shift: shift,
compact: true,
),
)
.toList(),
),
],
);
},
),
const SizedBox(height: UiConstants.space3),
// Tomorrow's Shifts
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
final shifts = state.tomorrowShifts;
return Column(
children: [
SectionHeader(title: sectionsI18n.tomorrow),
if (shifts.isEmpty)
EmptyStateWidget(
message: emptyI18n.no_shifts_tomorrow,
)
else
Column(
children: shifts
.map(
(shift) => ShiftCard(
shift: shift,
compact: true,
),
)
.toList(),
),
],
);
},
),
const SizedBox(height: UiConstants.space3),
// Recommended Shifts
SectionHeader(title: sectionsI18n.recommended_for_you),
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
if (state.recommendedShifts.isEmpty) {
return EmptyStateWidget(
message: emptyI18n.no_recommended_shifts,
);
}
return SizedBox(
height: 160,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: state.recommendedShifts.length,
clipBehavior: Clip.none,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(
right: UiConstants.space3,
),
child: RecommendedShiftCard(
shift: state.recommendedShifts[index],
), ),
), ),
)
else if (shifts.isEmpty)
EmptyStateWidget(
message: emptyI18n.no_shifts_today,
actionLink: emptyI18n.find_shifts_cta,
onAction: () =>
Modular.to.toShifts(initialTab: 'find'),
)
else
Column(
children: shifts
.map(
(shift) => ShiftCard(
shift: shift,
compact: true,
),
)
.toList(),
), ),
], );
); },
}, ),
), const SizedBox(height: UiConstants.space6),
const SizedBox(height: UiConstants.space3),
// Tomorrow's Shifts // Benefits
BlocBuilder<HomeCubit, HomeState>( BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) { buildWhen: (previous, current) =>
final shifts = state.tomorrowShifts; previous.benefits != current.benefits,
return Column( builder: (context, state) {
children: [ return BenefitsWidget(benefits: state.benefits);
SectionHeader(title: sectionsI18n.tomorrow), },
if (shifts.isEmpty) ),
EmptyStateWidget( const SizedBox(height: UiConstants.space6),
message: emptyI18n.no_shifts_tomorrow, ],
) );
else },
Column(
children: shifts
.map(
(shift) => ShiftCard(
shift: shift,
compact: true,
),
)
.toList(),
),
],
);
},
),
const SizedBox(height: UiConstants.space3),
// Recommended Shifts
SectionHeader(title: sectionsI18n.recommended_for_you),
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
if (state.recommendedShifts.isEmpty) {
return EmptyStateWidget(
message: emptyI18n.no_recommended_shifts,
);
}
return SizedBox(
height: 160,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: state.recommendedShifts.length,
clipBehavior: Clip.none,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(
right: UiConstants.space3,
),
child: RecommendedShiftCard(
shift: state.recommendedShifts[index],
),
),
),
);
},
),
const SizedBox(height: UiConstants.space6),
// Benefits
BlocBuilder<HomeCubit, HomeState>(
buildWhen: (previous, current) =>
previous.benefits != current.benefits,
builder: (context, state) {
return BenefitsWidget(benefits: state.benefits);
},
),
const SizedBox(height: UiConstants.space6),
],
), ),
), ),
], ],

View File

@@ -28,10 +28,7 @@ class BankAccountPage extends StatelessWidget {
final dynamic strings = t.staff.profile.bank_account_page; final dynamic strings = t.staff.profile.bank_account_page;
return Scaffold( return Scaffold(
appBar: UiAppBar( appBar: UiAppBar(title: strings.title, showBackButton: true),
title: strings.title,
showBackButton: true,
),
body: BlocConsumer<BankAccountCubit, BankAccountState>( body: BlocConsumer<BankAccountCubit, BankAccountState>(
bloc: cubit, bloc: cubit,
listener: (BuildContext context, BankAccountState state) { listener: (BuildContext context, BankAccountState state) {
@@ -81,34 +78,13 @@ class BankAccountPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
SecurityNotice(strings: strings), SecurityNotice(strings: strings),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space32),
if (state.accounts.isEmpty) if (state.accounts.isEmpty)
Center( const UiEmptyState(
child: Padding( icon: UiIcons.building,
padding: const EdgeInsets.symmetric( title: 'No accounts yet',
vertical: UiConstants.space10, description:
), 'Add your first bank account to get started',
child: Column(
children: <Widget>[
const Icon(
UiIcons.building,
size: 48,
color: UiColors.iconSecondary,
),
const SizedBox(height: UiConstants.space4),
Text(
'No accounts yet',
style: UiTypography.headline4m,
textAlign: TextAlign.center,
),
Text(
'Add your first bank account to get started',
style: UiTypography.body2m.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
) )
else ...<Widget>[ else ...<Widget>[
Text( Text(
@@ -119,10 +95,8 @@ class BankAccountPage extends StatelessWidget {
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
...state.accounts.map<Widget>( ...state.accounts.map<Widget>(
(StaffBankAccount account) => AccountCard( (StaffBankAccount account) =>
account: account, AccountCard(account: account, strings: strings),
strings: strings,
),
), ),
], ],
// Add extra padding at bottom // Add extra padding at bottom