refactor: extract attire UI components from pages into dedicated widgets for improved modularity.

This commit is contained in:
Achintha Isuru
2026-02-24 16:06:42 -05:00
parent 566b4e9839
commit bb27e3f8fe
8 changed files with 229 additions and 164 deletions

View File

@@ -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;
}

View File

@@ -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<AttireCapturePage> {
cubit.uploadPhoto(widget.item.id);
}
void _viewEnlargedImage(BuildContext context) {
showDialog<void>(
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<AttireCubit>();
@@ -97,46 +76,8 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
child: Column(
children: <Widget>[
// 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>[
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>[
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<AttireCapturePage> {
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: <Widget>[
const Icon(
UiIcons.info,
color: UiColors.primary,
size: 24,
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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<AttireCapturePage> {
const Center(child: CircularProgressIndicator())
else if (!hasPhoto ||
true) // Show options even if has photo (allows re-upload)
Row(
children: <Widget>[
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),
],
),
),

View File

@@ -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<AttirePage> {
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<AttireCubit>();
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<AttireCubit>.value(
value: cubit,
@@ -100,17 +69,13 @@ class _AttirePageState extends State<AttirePage> {
const SizedBox(height: UiConstants.space6),
// Filter Chips
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
_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<AttirePage> {
},
),
);
}).toList(),
}),
const SizedBox(height: UiConstants.space20),
],
),

View File

@@ -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<void>(
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>[
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>[Shadow(color: Colors.black, blurRadius: 4)],
),
),
),
),
);
}
}

View File

@@ -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: <Widget>[
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),
),
),
],
);
}
}

View File

@@ -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: <Widget>[
const Icon(UiIcons.info, color: UiColors.primary, size: 24),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Verification Status',
style: UiTypography.footnote2m.textPrimary,
),
Text(
statusText,
style: UiTypography.body2m.copyWith(color: statusColor),
),
],
),
),
],
),
);
}
}

View File

@@ -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<String> 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: <Widget>[
_buildFilterChip('All'),
const SizedBox(width: UiConstants.space2),
_buildFilterChip('Required'),
const SizedBox(width: UiConstants.space2),
_buildFilterChip('Non-Essential'),
],
),
);
}
}

View File

@@ -67,6 +67,7 @@ class AttireItemCard extends StatelessWidget {
],
const SizedBox(height: UiConstants.space2),
Row(
spacing: UiConstants.space2,
children: <Widget>[
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,