diff --git a/apps/mobile/packages/design_system/lib/design_system.dart b/apps/mobile/packages/design_system/lib/design_system.dart index d25f49f0..36c51fad 100644 --- a/apps/mobile/packages/design_system/lib/design_system.dart +++ b/apps/mobile/packages/design_system/lib/design_system.dart @@ -13,3 +13,4 @@ export 'src/widgets/ui_chip.dart'; export 'src/widgets/ui_loading_page.dart'; export 'src/widgets/ui_snackbar.dart'; export 'src/widgets/ui_notice_banner.dart'; +export 'src/widgets/ui_empty_state.dart'; diff --git a/apps/mobile/packages/design_system/lib/src/ui_typography.dart b/apps/mobile/packages/design_system/lib/src/ui_typography.dart index fde6263a..eb436569 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_typography.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_typography.dart @@ -131,6 +131,15 @@ class UiTypography { 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) static final TextStyle title2b = _primaryBase.copyWith( fontWeight: FontWeight.w600, diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_empty_state.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_empty_state.dart new file mode 100644 index 00000000..d719db2f --- /dev/null +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_empty_state.dart @@ -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: [ + 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, + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index 409dae51..1ba3ae29 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -61,12 +61,41 @@ class WorkerHomePage extends StatelessWidget { horizontal: UiConstants.space4, vertical: UiConstants.space4, ), - child: Column( - children: [ - BlocBuilder( - builder: (context, state) { - if (state.isProfileComplete) return const SizedBox(); - return PlaceholderBanner( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.isProfileComplete != current.isProfileComplete, + builder: (context, state) { + if (!state.isProfileComplete) { + 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, subtitle: bannersI18n.complete_profile_subtitle, bg: UiColors.primaryInverse, @@ -74,156 +103,156 @@ class WorkerHomePage extends StatelessWidget { onTap: () { 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 - BlocBuilder( - builder: (context, state) { - final shifts = state.todayShifts; - return Column( + const SizedBox(height: UiConstants.space6), + + // Quick Actions + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SectionHeader( - title: sectionsI18n.todays_shift, - action: shifts.isNotEmpty - ? sectionsI18n.scheduled_count( - count: shifts.length, - ) - : null, + Expanded( + child: QuickActionItem( + icon: UiIcons.search, + label: quickI18n.find_shifts, + onTap: () => Modular.to.toShifts(), + ), ), - if (state.status == HomeStatus.loading) - const Center( - child: SizedBox( - height: UiConstants.space10, - width: UiConstants.space10, - child: CircularProgressIndicator( - color: UiColors.primary, + 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 + BlocBuilder( + 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( + 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( + 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.space3), + ); + }, + ), + const SizedBox(height: UiConstants.space6), - // Tomorrow's Shifts - BlocBuilder( - 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( - 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( - buildWhen: (previous, current) => - previous.benefits != current.benefits, - builder: (context, state) { - return BenefitsWidget(benefits: state.benefits); - }, - ), - const SizedBox(height: UiConstants.space6), - ], + // Benefits + BlocBuilder( + buildWhen: (previous, current) => + previous.benefits != current.benefits, + builder: (context, state) { + return BenefitsWidget(benefits: state.benefits); + }, + ), + const SizedBox(height: UiConstants.space6), + ], + ); + }, ), ), ], diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart index 3f814544..b81bae6f 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -28,10 +28,7 @@ class BankAccountPage extends StatelessWidget { final dynamic strings = t.staff.profile.bank_account_page; return Scaffold( - appBar: UiAppBar( - title: strings.title, - showBackButton: true, - ), + appBar: UiAppBar(title: strings.title, showBackButton: true), body: BlocConsumer( bloc: cubit, listener: (BuildContext context, BankAccountState state) { @@ -81,34 +78,13 @@ class BankAccountPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SecurityNotice(strings: strings), - const SizedBox(height: UiConstants.space6), + const SizedBox(height: UiConstants.space32), if (state.accounts.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space10, - ), - child: Column( - children: [ - 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, - ), - ], - ), - ), + const UiEmptyState( + icon: UiIcons.building, + title: 'No accounts yet', + description: + 'Add your first bank account to get started', ) else ...[ Text( @@ -119,10 +95,8 @@ class BankAccountPage extends StatelessWidget { ), const SizedBox(height: UiConstants.space3), ...state.accounts.map( - (StaffBankAccount account) => AccountCard( - account: account, - strings: strings, - ), + (StaffBankAccount account) => + AccountCard(account: account, strings: strings), ), ], // Add extra padding at bottom