From ce095924bc7a69f0ed295094ef8bbb695e2c81a0 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 28 Feb 2026 22:42:04 -0500 Subject: [PATCH] feat: Implement notice and file types banners for attire upload and enhance incomplete profile messaging --- .../lib/src/l10n/en.i18n.json | 2 +- .../lib/src/l10n/es.i18n.json | 2 +- .../design_system/lib/design_system.dart | 1 + .../lib/src/widgets/ui_notice_banner.dart | 81 ++++++++++++++++++ .../pages/attire_capture_page.dart | 35 +------- .../file_types_banner.dart | 36 ++++++++ .../widgets/attire_info_card.dart | 33 +------- .../widgets/tabs/find_shifts_tab.dart | 82 +++++-------------- 8 files changed, 145 insertions(+), 127 deletions(-) create mode 100644 apps/mobile/packages/design_system/lib/src/widgets/ui_notice_banner.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/file_types_banner.dart diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index e0544de4..52fbdc50 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -1318,7 +1318,7 @@ }, "find_shifts": { "incomplete_profile_banner_title": "Your account isn't complete yet.", - "incomplete_profile_banner_message": "You won't be able to apply for shifts until your account is fully set up. Complete your account now to unlock shift applications and start getting matched with opportunities.", + "incomplete_profile_banner_message": "Complete your account now to unlock shift applications and start getting matched with opportunities.", "incomplete_profile_cta": "Complete your account now", "search_hint": "Search jobs, location...", "filter_all": "All Jobs", diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 599bfa23..3e057580 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -1313,7 +1313,7 @@ }, "find_shifts": { "incomplete_profile_banner_title": "Tu cuenta aún no está completa.", - "incomplete_profile_banner_message": "No podrás solicitar turnos hasta que tu cuenta esté completamente configurada. Completa tu cuenta ahora para desbloquear las solicitudes de turnos y empezar a recibir oportunidades.", + "incomplete_profile_banner_message": "Completa tu cuenta ahora para desbloquear las solicitudes de turnos y empezar a recibir oportunidades.", "incomplete_profile_cta": "Completa tu cuenta ahora", "search_hint": "Buscar trabajos, ubicaci\u00f3n...", "filter_all": "Todos", diff --git a/apps/mobile/packages/design_system/lib/design_system.dart b/apps/mobile/packages/design_system/lib/design_system.dart index 2bfc01d4..d25f49f0 100644 --- a/apps/mobile/packages/design_system/lib/design_system.dart +++ b/apps/mobile/packages/design_system/lib/design_system.dart @@ -12,3 +12,4 @@ export 'src/widgets/ui_button.dart'; 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'; diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_notice_banner.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_notice_banner.dart new file mode 100644 index 00000000..445e8141 --- /dev/null +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_notice_banner.dart @@ -0,0 +1,81 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../ui_constants.dart'; + +/// A customizable notice banner widget for displaying informational messages. +/// +/// [UiNoticeBanner] displays a message with an optional icon and supports +/// custom styling through title and description text. +class UiNoticeBanner extends StatelessWidget { + /// Creates a [UiNoticeBanner]. + const UiNoticeBanner({ + super.key, + this.icon, + required this.title, + this.description, + this.backgroundColor, + this.borderRadius, + this.padding, + }); + + /// The icon to display on the left side. + /// Defaults to null. The icon will be rendered with primary color and 24pt size. + final IconData? icon; + + /// The title text to display. + final String title; + + /// Optional description text to display below the title. + final String? description; + + /// The background color of the banner. + /// Defaults to [UiColors.primary] with 8% opacity. + final Color? backgroundColor; + + /// The border radius of the banner. + /// Defaults to [UiConstants.radiusLg]. + final BorderRadius? borderRadius; + + /// The padding around the banner content. + /// Defaults to [UiConstants.space4] on all sides. + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Container( + padding: padding ?? const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: backgroundColor ?? UiColors.primary.withValues(alpha: 0.08), + borderRadius: borderRadius ?? UiConstants.radiusLg, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (icon != null) ...[ + Icon(icon, color: UiColors.primary, size: 24), + const SizedBox(width: UiConstants.space3), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: UiTypography.body2m.textPrimary, + ), + if (description != null) ...[ + const SizedBox(height: 2), + Text( + description!, + style: UiTypography.body2r.textSecondary, + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart index 1f0b60f1..243c2b65 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart @@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart'; import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart'; import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_state.dart'; +import '../widgets/attire_capture_page/file_types_banner.dart'; import '../widgets/attire_capture_page/footer_section.dart'; import '../widgets/attire_capture_page/image_preview_section.dart'; import '../widgets/attire_capture_page/info_section.dart'; @@ -286,7 +287,7 @@ class _AttireCapturePageState extends State { padding: const EdgeInsets.all(UiConstants.space5), child: Column( children: [ - _FileTypesBanner( + FileTypesBanner( message: t .staff_profile_attire .upload_file_types_banner, @@ -335,35 +336,3 @@ class _AttireCapturePageState extends State { ); } } - -/// Banner displaying accepted file types and size limit for attire upload. -class _FileTypesBanner extends StatelessWidget { - const _FileTypesBanner({required this.message}); - - final String message; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - decoration: BoxDecoration( - color: UiColors.primary.withAlpha(20), - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(UiIcons.info, size: 20, color: UiColors.primary), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Text(message, style: UiTypography.body2r.textSecondary), - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/file_types_banner.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/file_types_banner.dart new file mode 100644 index 00000000..4791f4d5 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/file_types_banner.dart @@ -0,0 +1,36 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Banner displaying accepted file types and size limit for attire upload. +class FileTypesBanner extends StatelessWidget { + /// Creates a [FileTypesBanner]. + const FileTypesBanner({super.key, required this.message}); + + /// The message to display in the banner. + final String message; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: UiColors.primary.withAlpha(20), + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(UiIcons.info, size: 20, color: UiColors.primary), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Text(message, style: UiTypography.body2r.textSecondary), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_info_card.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_info_card.dart index d7e12dc4..f1c5cd46 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_info_card.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_info_card.dart @@ -7,35 +7,10 @@ class AttireInfoCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.primary.withValues(alpha: 0.08), - borderRadius: UiConstants.radiusLg, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(UiIcons.shirt, color: UiColors.primary, size: 24), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - t.staff_profile_attire.info_card.title, - style: UiTypography.body2m.textPrimary, - ), - const SizedBox(height: 2), - Text( - t.staff_profile_attire.info_card.description, - style: UiTypography.body2r.textSecondary, - ), - ], - ), - ), - ], - ), + return UiNoticeBanner( + icon: UiIcons.shirt, + title: t.staff_profile_attire.info_card.title, + description: t.staff_profile_attire.info_card.description, ); } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 592e03a9..726bc560 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -1,14 +1,13 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:krow_domain/krow_domain.dart'; -import 'package:core_localization/core_localization.dart'; + import '../../blocs/shifts/shifts_bloc.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; -import 'package:geolocator/geolocator.dart'; class FindShiftsTab extends StatefulWidget { final List availableJobs; @@ -316,11 +315,21 @@ class _FindShiftsTabState extends State { children: [ // Incomplete profile banner if (!widget.profileComplete) ...[ - _IncompleteProfileBanner( - title: context.t.staff_shifts.find_shifts.incomplete_profile_banner_title, - message: context.t.staff_shifts.find_shifts.incomplete_profile_banner_message, - ctaText: context.t.staff_shifts.find_shifts.incomplete_profile_cta, - onCtaPressed: () => Modular.to.toProfile(), + Container( + padding: const EdgeInsets.all(UiConstants.space4), + child: UiNoticeBanner( + icon: UiIcons.sparkles, + title: context + .t + .staff_shifts + .find_shifts + .incomplete_profile_banner_title, + description: context + .t + .staff_shifts + .find_shifts + .incomplete_profile_banner_message, + ), ), ], // Search and Filters @@ -462,9 +471,7 @@ class _FindShiftsTabState extends State { ? () { BlocProvider.of( context, - ).add( - AcceptShiftEvent(shift.id), - ); + ).add(AcceptShiftEvent(shift.id)); UiSnackbar.show( context, message: context @@ -488,54 +495,3 @@ class _FindShiftsTabState extends State { ); } } - -/// Banner shown when the worker's profile is incomplete. -class _IncompleteProfileBanner extends StatelessWidget { - const _IncompleteProfileBanner({ - required this.title, - required this.message, - required this.ctaText, - required this.onCtaPressed, - }); - - final String title; - final String message; - final String ctaText; - final VoidCallback onCtaPressed; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.all(UiConstants.space5), - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.tagPending, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all( - color: UiColors.textWarning.withValues(alpha: 0.5), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: UiTypography.body2b.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - Text( - message, - style: UiTypography.body3r.textSecondary, - ), - const SizedBox(height: UiConstants.space4), - UiButton.primary( - text: ctaText, - onPressed: onCtaPressed, - size: UiButtonSize.small, - ), - ], - ), - ); - } -}