From bb27e3f8feb199c7ce2e980f0a2fa9fc4e510e95 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 24 Feb 2026 16:06:42 -0500 Subject: [PATCH] refactor: extract attire UI components from pages into dedicated widgets for improved modularity. --- .../design_system/lib/src/ui_icons.dart | 3 + .../pages/attire_capture_page.dart | 126 ++---------------- .../src/presentation/pages/attire_page.dart | 57 ++------ .../attire_image_preview.dart | 72 ++++++++++ .../attire_upload_buttons.dart | 31 +++++ .../attire_verification_status_card.dart | 46 +++++++ .../widgets/attire_filter_chips.dart | 56 ++++++++ .../widgets/attire_item_card.dart | 2 +- 8 files changed, 229 insertions(+), 164 deletions(-) create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_image_preview.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_verification_status_card.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_filter_chips.dart diff --git a/apps/mobile/packages/design_system/lib/src/ui_icons.dart b/apps/mobile/packages/design_system/lib/src/ui_icons.dart index 6aac02b2..537ef4f7 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -276,4 +276,7 @@ class UiIcons { /// Help circle icon for FAQs static const IconData helpCircle = _IconLib.helpCircle; + + /// Gallery icon for gallery + static const IconData gallery = _IconLib.galleryVertical; } 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 fd68a50e..d314b6d0 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 @@ -8,6 +8,9 @@ import 'package:core_localization/core_localization.dart'; import '../blocs/attire_cubit.dart'; import '../blocs/attire_state.dart'; import '../widgets/attestation_checkbox.dart'; +import '../widgets/attire_capture_page/attire_image_preview.dart'; +import '../widgets/attire_capture_page/attire_upload_buttons.dart'; +import '../widgets/attire_capture_page/attire_verification_status_card.dart'; class AttireCapturePage extends StatefulWidget { const AttireCapturePage({super.key, required this.item}); @@ -36,30 +39,6 @@ class _AttireCapturePageState extends State { cubit.uploadPhoto(widget.item.id); } - void _viewEnlargedImage(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - child: Container( - constraints: const BoxConstraints(maxHeight: 500, maxWidth: 500), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - image: DecorationImage( - image: NetworkImage( - widget.item.imageUrl ?? - 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', - ), - fit: BoxFit.contain, - ), - ), - ), - ); - }, - ); - } - @override Widget build(BuildContext context) { final AttireCubit cubit = Modular.get(); @@ -97,46 +76,8 @@ class _AttireCapturePageState extends State { child: Column( children: [ // Image Preview - GestureDetector( - onTap: () => _viewEnlargedImage(context), - child: Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - boxShadow: const [ - BoxShadow( - color: Color(0x19000000), - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - image: DecorationImage( - image: NetworkImage( - widget.item.imageUrl ?? - 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', - ), - fit: BoxFit.cover, - ), - ), - child: const Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - UiIcons.search, - color: UiColors.white, - shadows: [ - Shadow(color: Colors.black, blurRadius: 4), - ], - ), - ), - ), - ), - ), + // Image Preview + AttireImagePreview(imageUrl: widget.item.imageUrl), const SizedBox(height: UiConstants.space6), Text( @@ -147,42 +88,9 @@ class _AttireCapturePageState extends State { const SizedBox(height: UiConstants.space6), // Verification info - Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.bgPopup, - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - border: Border.all(color: UiColors.border), - ), - child: Row( - children: [ - const Icon( - UiIcons.info, - color: UiColors.primary, - size: 24, - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Verification Status', - style: UiTypography.footnote2m.textPrimary, - ), - Text( - statusText, - style: UiTypography.body2m.copyWith( - color: statusColor, - ), - ), - ], - ), - ), - ], - ), + AttireVerificationStatusCard( + statusText: statusText, + statusColor: statusColor, ), const SizedBox(height: UiConstants.space6), @@ -200,23 +108,7 @@ class _AttireCapturePageState extends State { const Center(child: CircularProgressIndicator()) else if (!hasPhoto || true) // Show options even if has photo (allows re-upload) - Row( - children: [ - Expanded( - child: UiButton.secondary( - text: 'Gallery', - onPressed: () => _onUpload(context), - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: UiButton.primary( - text: 'Camera', - onPressed: () => _onUpload(context), - ), - ), - ], - ), + AttireUploadButtons(onUpload: _onUpload), ], ), ), diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart index 7e17a08b..7d3aaa34 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart @@ -1,15 +1,16 @@ +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:core_localization/core_localization.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../blocs/attire_cubit.dart'; import '../blocs/attire_state.dart'; +import '../widgets/attire_filter_chips.dart'; import '../widgets/attire_info_card.dart'; import '../widgets/attire_item_card.dart'; import 'attire_capture_page.dart'; -import 'package:krow_domain/krow_domain.dart'; class AttirePage extends StatefulWidget { const AttirePage({super.key}); @@ -21,46 +22,14 @@ class AttirePage extends StatefulWidget { class _AttirePageState extends State { String _filter = 'All'; - Widget _buildFilterChip(String label) { - final bool isSelected = _filter == label; - return GestureDetector( - onTap: () => setState(() => _filter = label), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space2, - ), - decoration: BoxDecoration( - color: isSelected ? UiColors.primary : UiColors.white, - borderRadius: UiConstants.radiusFull, - border: Border.all( - color: isSelected ? UiColors.primary : UiColors.border, - ), - ), - child: Text( - label, - textAlign: TextAlign.center, - style: (isSelected - ? UiTypography.footnote2m.white - : UiTypography.footnote2m.textSecondary), - ), - ), - ); - } - @override Widget build(BuildContext context) { final AttireCubit cubit = Modular.get(); return Scaffold( - backgroundColor: UiColors.background, appBar: UiAppBar( title: t.staff_profile_attire.title, showBackButton: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), ), body: BlocProvider.value( value: cubit, @@ -100,17 +69,13 @@ class _AttirePageState extends State { const SizedBox(height: UiConstants.space6), // Filter Chips - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _buildFilterChip('All'), - const SizedBox(width: UiConstants.space2), - _buildFilterChip('Required'), - const SizedBox(width: UiConstants.space2), - _buildFilterChip('Non-Essential'), - ], - ), + AttireFilterChips( + selectedFilter: _filter, + onFilterChanged: (String value) { + setState(() { + _filter = value; + }); + }, ), const SizedBox(height: UiConstants.space6), @@ -136,7 +101,7 @@ class _AttirePageState extends State { }, ), ); - }).toList(), + }), const SizedBox(height: UiConstants.space20), ], ), diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_image_preview.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_image_preview.dart new file mode 100644 index 00000000..5adfeec2 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_image_preview.dart @@ -0,0 +1,72 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireImagePreview extends StatelessWidget { + const AttireImagePreview({super.key, required this.imageUrl}); + + final String? imageUrl; + + void _viewEnlargedImage(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + constraints: const BoxConstraints(maxHeight: 500, maxWidth: 500), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + image: DecorationImage( + image: NetworkImage( + imageUrl ?? + 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', + ), + fit: BoxFit.contain, + ), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => _viewEnlargedImage(context), + child: Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + boxShadow: const [ + BoxShadow( + color: Color(0x19000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + image: DecorationImage( + image: NetworkImage( + imageUrl ?? + 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', + ), + fit: BoxFit.cover, + ), + ), + child: const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + UiIcons.search, + color: UiColors.white, + shadows: [Shadow(color: Colors.black, blurRadius: 4)], + ), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart new file mode 100644 index 00000000..83067e7e --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart @@ -0,0 +1,31 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireUploadButtons extends StatelessWidget { + const AttireUploadButtons({super.key, required this.onUpload}); + + final void Function(BuildContext) onUpload; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: UiButton.secondary( + leadingIcon: UiIcons.gallery, + text: 'Gallery', + onPressed: () => onUpload(context), + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: UiButton.primary( + leadingIcon: UiIcons.camera, + text: 'Camera', + onPressed: () => onUpload(context), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_verification_status_card.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_verification_status_card.dart new file mode 100644 index 00000000..2799aea2 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_verification_status_card.dart @@ -0,0 +1,46 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireVerificationStatusCard extends StatelessWidget { + const AttireVerificationStatusCard({ + super.key, + required this.statusText, + required this.statusColor, + }); + + final String statusText; + final Color statusColor; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all(color: UiColors.border), + ), + child: Row( + children: [ + const Icon(UiIcons.info, color: UiColors.primary, size: 24), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Verification Status', + style: UiTypography.footnote2m.textPrimary, + ), + Text( + statusText, + style: UiTypography.body2m.copyWith(color: statusColor), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_filter_chips.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_filter_chips.dart new file mode 100644 index 00000000..b7ca10eb --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_filter_chips.dart @@ -0,0 +1,56 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireFilterChips extends StatelessWidget { + const AttireFilterChips({ + super.key, + required this.selectedFilter, + required this.onFilterChanged, + }); + + final String selectedFilter; + final ValueChanged onFilterChanged; + + Widget _buildFilterChip(String label) { + final bool isSelected = selectedFilter == label; + return GestureDetector( + onTap: () => onFilterChanged(label), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space2, + ), + decoration: BoxDecoration( + color: isSelected ? UiColors.primary : UiColors.white, + borderRadius: UiConstants.radiusFull, + border: Border.all( + color: isSelected ? UiColors.primary : UiColors.border, + ), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: (isSelected + ? UiTypography.footnote2m.white + : UiTypography.footnote2m.textSecondary), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildFilterChip('All'), + const SizedBox(width: UiConstants.space2), + _buildFilterChip('Required'), + const SizedBox(width: UiConstants.space2), + _buildFilterChip('Non-Essential'), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart index d13bb8e1..005fe6a2 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart @@ -67,6 +67,7 @@ class AttireItemCard extends StatelessWidget { ], const SizedBox(height: UiConstants.space2), Row( + spacing: UiConstants.space2, children: [ if (item.isMandatory) const UiChip( @@ -74,7 +75,6 @@ class AttireItemCard extends StatelessWidget { size: UiChipSize.xSmall, variant: UiChipVariant.destructive, ), - const Spacer(), if (isUploading) const SizedBox( width: 16,