From e084dad4a7d62956a94c1ed64cc9d98a276f5e22 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 24 Feb 2026 13:59:55 -0500 Subject: [PATCH] feat: Refactor client hubs to centralize hub actions and update UI styling. --- .../lib/src/widgets/ui_app_bar.dart | 18 ++- .../src/presentation/pages/edit_hub_page.dart | 143 +++++++++--------- .../presentation/pages/hub_details_page.dart | 141 ++++++++++------- 3 files changed, 178 insertions(+), 124 deletions(-) diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_app_bar.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_app_bar.dart index f3f4040e..46654038 100644 --- a/apps/mobile/packages/design_system/lib/src/widgets/ui_app_bar.dart +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_app_bar.dart @@ -8,6 +8,7 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget { const UiAppBar({ super.key, this.title, + this.subtitle, this.titleWidget, this.leading, this.actions, @@ -21,6 +22,9 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget { /// The title text to display in the app bar. final String? title; + /// The subtitle text to display in the app bar. + final String? subtitle; + /// A widget to display instead of the title text. final Widget? titleWidget; @@ -53,7 +57,19 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( title: titleWidget ?? - (title != null ? Text(title!, style: UiTypography.headline4b) : null), + (title != null + ? Column( + crossAxisAlignment: centerTitle + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title!, style: UiTypography.headline4b), + if (subtitle != null) + Text(subtitle!, style: UiTypography.body3r.textSecondary), + ], + ) + : null), leading: leading ?? (showBackButton diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart index d230c1ba..3e9a1f15 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart @@ -35,6 +35,10 @@ class _EditHubPageState extends State { _nameController = TextEditingController(text: widget.hub?.name); _addressController = TextEditingController(text: widget.hub?.address); _addressFocusNode = FocusNode(); + + // Update header on change + _nameController.addListener(() => setState(() {})); + _addressController.addListener(() => setState(() {})); } @override @@ -115,84 +119,79 @@ class _EditHubPageState extends State { return Scaffold( backgroundColor: UiColors.bgMenu, - appBar: AppBar( - backgroundColor: UiColors.foreground, - leading: IconButton( - icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), - onPressed: () => Modular.to.pop(), - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.hub == null - ? t.client_hubs.add_hub_dialog.title - : t.client_hubs.edit_hub.title, - style: UiTypography.headline3m.white, - ), - Text( - widget.hub == null - ? t.client_hubs.add_hub_dialog.create_button - : t.client_hubs.edit_hub.subtitle, - style: UiTypography.footnote1r.copyWith( - color: UiColors.white.withValues(alpha: 0.7), - ), - ), - ], - ), + appBar: UiAppBar( + title: widget.hub == null + ? t.client_hubs.add_hub_dialog.title + : t.client_hubs.edit_hub.title, + subtitle: widget.hub == null + ? t.client_hubs.add_hub_dialog.create_button + : t.client_hubs.edit_hub.subtitle, + onLeadingPressed: () => Modular.to.pop(), ), body: Stack( children: [ SingleChildScrollView( - padding: const EdgeInsets.all(UiConstants.space5), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // ── Name field ────────────────────────────────── - _FieldLabel(t.client_hubs.edit_hub.name_label), - TextFormField( - controller: _nameController, - style: UiTypography.body1r.textPrimary, - textInputAction: TextInputAction.next, - validator: (String? value) { - if (value == null || value.trim().isEmpty) { - return 'Name is required'; - } - return null; - }, - decoration: _inputDecoration( - t.client_hubs.edit_hub.name_hint, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Name field ────────────────────────────────── + _FieldLabel(t.client_hubs.edit_hub.name_label), + TextFormField( + controller: _nameController, + style: UiTypography.body1r.textPrimary, + textInputAction: TextInputAction.next, + validator: (String? value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required'; + } + return null; + }, + decoration: _inputDecoration( + t.client_hubs.edit_hub.name_hint, + ), + ), + + const SizedBox(height: UiConstants.space4), + + // ── Address field ──────────────────────────────── + _FieldLabel( + t.client_hubs.edit_hub.address_label, + ), + HubAddressAutocomplete( + controller: _addressController, + hintText: t.client_hubs.edit_hub.address_hint, + focusNode: _addressFocusNode, + onSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, + ), + + const SizedBox(height: UiConstants.space8), + + // ── Save button ────────────────────────────────── + UiButton.primary( + onPressed: isSaving ? null : _onSave, + text: widget.hub == null + ? t + .client_hubs + .add_hub_dialog + .create_button + : t.client_hubs.edit_hub.save_button, + ), + + const SizedBox(height: 40), + ], ), ), - - const SizedBox(height: UiConstants.space4), - - // ── Address field ──────────────────────────────── - _FieldLabel(t.client_hubs.edit_hub.address_label), - HubAddressAutocomplete( - controller: _addressController, - hintText: t.client_hubs.edit_hub.address_hint, - focusNode: _addressFocusNode, - onSelected: (Prediction prediction) { - _selectedPrediction = prediction; - }, - ), - - const SizedBox(height: UiConstants.space8), - - // ── Save button ────────────────────────────────── - UiButton.primary( - onPressed: isSaving ? null : _onSave, - text: widget.hub == null - ? t.client_hubs.add_hub_dialog.create_button - : t.client_hubs.edit_hub.save_button, - ), - - const SizedBox(height: 40), - ], - ), + ), + ], ), ), diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart index 397ca883..d6d41786 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart @@ -43,57 +43,105 @@ class HubDetailsPage extends StatelessWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(hub.name), - backgroundColor: UiColors.foreground, - leading: IconButton( - icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), - onPressed: () => Modular.to.pop(), - ), + appBar: UiAppBar( + title: hub.name, + subtitle: t.client_hubs.hub_details.title, + onLeadingPressed: () => Modular.to.pop(), actions: [ IconButton( onPressed: () => _confirmDeleteHub(context), - icon: const Icon( - UiIcons.delete, - color: UiColors.white, - size: 20, - ), + icon: const Icon(UiIcons.delete, color: UiColors.iconSecondary), ), - TextButton.icon( - onPressed: () => _navigateToEditPage(context), - icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16), - label: Text( - t.client_hubs.hub_details.edit_button, - style: const TextStyle(color: UiColors.white), - ), + UiIconButton( + icon: UiIcons.edit, + onTap: () => _navigateToEditPage(context), + backgroundColor: UiColors.transparent, + iconColor: UiColors.iconSecondary, ), ], ), backgroundColor: UiColors.bgMenu, - body: Padding( - padding: const EdgeInsets.all(UiConstants.space5), + body: SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildDetailItem( - label: t.client_hubs.hub_details.name_label, - value: hub.name, - icon: UiIcons.home, + // ── Header ────────────────────────────────────────── + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + width: 114, + decoration: BoxDecoration( + color: UiColors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.primary), + ), + child: const Center( + child: Icon( + UiIcons.nfc, + color: UiColors.primary, + size: 32, + ), + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + hub.name, + style: UiTypography.headline1b.textPrimary, + ), + const SizedBox(height: UiConstants.space1), + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 16, + color: UiColors.textSecondary, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + hub.address, + style: UiTypography.body2r.textSecondary, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), ), - const SizedBox(height: UiConstants.space4), - _buildDetailItem( - label: t.client_hubs.hub_details.address_label, - value: hub.address, - icon: UiIcons.mapPin, - ), - const SizedBox(height: UiConstants.space4), - _buildDetailItem( - label: t.client_hubs.hub_details.nfc_label, - value: - hub.nfcTagId ?? - t.client_hubs.hub_details.nfc_not_assigned, - icon: UiIcons.nfc, - isHighlight: hub.nfcTagId != null, + const Divider(height: 1, thickness: 0.5), + + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildDetailItem( + label: t.client_hubs.hub_details.nfc_label, + value: + hub.nfcTagId ?? + t.client_hubs.hub_details.nfc_not_assigned, + icon: UiIcons.nfc, + isHighlight: hub.nfcTagId != null, + ), + ], + ), ), ], ), @@ -114,13 +162,7 @@ class HubDetailsPage extends StatelessWidget { decoration: BoxDecoration( color: UiColors.white, borderRadius: BorderRadius.circular(UiConstants.radiusBase), - boxShadow: const [ - BoxShadow( - color: UiColors.popupShadow, - blurRadius: 10, - offset: Offset(0, 4), - ), - ], + border: Border.all(color: UiColors.border), ), child: Row( children: [ @@ -134,7 +176,7 @@ class HubDetailsPage extends StatelessWidget { ), child: Icon( icon, - color: isHighlight ? UiColors.iconSuccess : UiColors.iconPrimary, + color: isHighlight ? UiColors.iconSuccess : UiColors.iconThird, size: 20, ), ), @@ -155,9 +197,6 @@ class HubDetailsPage extends StatelessWidget { } Future _navigateToEditPage(BuildContext context) async { - // We still need to pass a Bloc for the edit page, but it's handled by Modular. - // However, the Navigator extension expect a Bloc. - // I'll update the Navigator extension to NOT require a Bloc since it's in Modular. final bool? saved = await Modular.to.toEditHub(hub: hub); if (saved == true && context.mounted) { Modular.to.pop(true); // Return true to indicate change