feat: Implement notice and file types banners for attire upload and enhance incomplete profile messaging

This commit is contained in:
Achintha Isuru
2026-02-28 22:42:04 -05:00
parent 1ab5ba2e6f
commit ce095924bc
8 changed files with 145 additions and 127 deletions

View File

@@ -1318,7 +1318,7 @@
}, },
"find_shifts": { "find_shifts": {
"incomplete_profile_banner_title": "Your account isn't complete yet.", "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", "incomplete_profile_cta": "Complete your account now",
"search_hint": "Search jobs, location...", "search_hint": "Search jobs, location...",
"filter_all": "All Jobs", "filter_all": "All Jobs",

View File

@@ -1313,7 +1313,7 @@
}, },
"find_shifts": { "find_shifts": {
"incomplete_profile_banner_title": "Tu cuenta aún no está completa.", "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", "incomplete_profile_cta": "Completa tu cuenta ahora",
"search_hint": "Buscar trabajos, ubicaci\u00f3n...", "search_hint": "Buscar trabajos, ubicaci\u00f3n...",
"filter_all": "Todos", "filter_all": "Todos",

View File

@@ -12,3 +12,4 @@ export 'src/widgets/ui_button.dart';
export 'src/widgets/ui_chip.dart'; 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';

View File

@@ -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: <Widget>[
if (icon != null) ...<Widget>[
Icon(icon, color: UiColors.primary, size: 24),
const SizedBox(width: UiConstants.space3),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.body2m.textPrimary,
),
if (description != null) ...<Widget>[
const SizedBox(height: 2),
Text(
description!,
style: UiTypography.body2r.textSecondary,
),
],
],
),
),
],
),
);
}
}

View File

@@ -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_cubit.dart';
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_state.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/footer_section.dart';
import '../widgets/attire_capture_page/image_preview_section.dart'; import '../widgets/attire_capture_page/image_preview_section.dart';
import '../widgets/attire_capture_page/info_section.dart'; import '../widgets/attire_capture_page/info_section.dart';
@@ -286,7 +287,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
_FileTypesBanner( FileTypesBanner(
message: t message: t
.staff_profile_attire .staff_profile_attire
.upload_file_types_banner, .upload_file_types_banner,
@@ -335,35 +336,3 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
); );
} }
} }
/// 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: <Widget>[
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(message, style: UiTypography.body2r.textSecondary),
),
],
),
);
}
}

View File

@@ -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: <Widget>[
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(message, style: UiTypography.body2r.textSecondary),
),
],
),
);
}
}

View File

@@ -7,35 +7,10 @@ class AttireInfoCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return UiNoticeBanner(
padding: const EdgeInsets.all(UiConstants.space4), icon: UiIcons.shirt,
decoration: BoxDecoration( title: t.staff_profile_attire.info_card.title,
color: UiColors.primary.withValues(alpha: 0.08), description: t.staff_profile_attire.info_card.description,
borderRadius: UiConstants.radiusLg,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Icon(UiIcons.shirt, color: UiColors.primary, size: 24),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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,
),
],
),
),
],
),
); );
} }
} }

View File

@@ -1,14 +1,13 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; 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:flutter_modular/flutter_modular.dart'; import 'package:geolocator/geolocator.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
import '../../blocs/shifts/shifts_bloc.dart'; import '../../blocs/shifts/shifts_bloc.dart';
import '../my_shift_card.dart'; import '../my_shift_card.dart';
import '../shared/empty_state_view.dart'; import '../shared/empty_state_view.dart';
import 'package:geolocator/geolocator.dart';
class FindShiftsTab extends StatefulWidget { class FindShiftsTab extends StatefulWidget {
final List<Shift> availableJobs; final List<Shift> availableJobs;
@@ -316,11 +315,21 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
children: [ children: [
// Incomplete profile banner // Incomplete profile banner
if (!widget.profileComplete) ...[ if (!widget.profileComplete) ...[
_IncompleteProfileBanner( Container(
title: context.t.staff_shifts.find_shifts.incomplete_profile_banner_title, padding: const EdgeInsets.all(UiConstants.space4),
message: context.t.staff_shifts.find_shifts.incomplete_profile_banner_message, child: UiNoticeBanner(
ctaText: context.t.staff_shifts.find_shifts.incomplete_profile_cta, icon: UiIcons.sparkles,
onCtaPressed: () => Modular.to.toProfile(), 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 // Search and Filters
@@ -462,9 +471,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
? () { ? () {
BlocProvider.of<ShiftsBloc>( BlocProvider.of<ShiftsBloc>(
context, context,
).add( ).add(AcceptShiftEvent(shift.id));
AcceptShiftEvent(shift.id),
);
UiSnackbar.show( UiSnackbar.show(
context, context,
message: context message: context
@@ -488,54 +495,3 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
); );
} }
} }
/// 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: <Widget>[
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,
),
],
),
);
}
}