feat: Add UiEmptyState widget and integrate it into BankAccountPage and WorkerHomePage for improved empty state handling
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user