refactor: extract UI components into dedicated widgets for the client settings page and update repository constructor.

This commit is contained in:
Achintha Isuru
2026-01-21 19:41:13 -05:00
parent eace8a66af
commit 78917a5f84
8 changed files with 256 additions and 186 deletions

View File

@@ -15,7 +15,7 @@ class ClientSettingsModule extends Module {
void binds(Injector i) {
// Repositories
i.addLazySingleton<SettingsRepositoryInterface>(
() => SettingsRepositoryImpl(i.get<AuthRepositoryMock>()),
() => SettingsRepositoryImpl(mock: i.get<AuthRepositoryMock>()),
);
// UseCases

View File

@@ -1,15 +1,19 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import '../../domain/repositories/settings_repository_interface.dart';
/// Implementation of [SettingsRepositoryInterface] that delegates to [AuthRepositoryMock].
/// Implementation of [SettingsRepositoryInterface].
///
/// This implementation delegates data access to the [AuthRepositoryMock]
/// from the `data_connect` package.
class SettingsRepositoryImpl implements SettingsRepositoryInterface {
final AuthRepositoryMock _authMock;
/// The auth mock from data connect.
final AuthRepositoryMock mock;
/// Creates a [SettingsRepositoryImpl].
SettingsRepositoryImpl(this._authMock);
/// Creates a [SettingsRepositoryImpl] with the required [mock].
SettingsRepositoryImpl({required this.mock});
@override
Future<void> signOut() {
return _authMock.signOut();
return mock.signOut();
}
}

View File

@@ -1,5 +1,7 @@
/// Interface for the Client Settings Repository.
/// Interface for the Client Settings repository.
///
/// This repository handles settings-related operations such as user sign out.
abstract interface class SettingsRepositoryInterface {
/// Signs out the current user.
/// Signs out the current user from the application.
Future<void> signOut();
}

View File

@@ -2,10 +2,14 @@ import 'package:krow_core/core.dart';
import '../repositories/settings_repository_interface.dart';
/// Use case handles the user sign out process.
///
/// This use case delegates the sign out logic to the [SettingsRepositoryInterface].
class SignOutUseCase implements NoInputUseCase<void> {
final SettingsRepositoryInterface _repository;
/// Creates a [SignOutUseCase].
///
/// Requires a [SettingsRepositoryInterface] to perform the sign out operation.
SignOutUseCase(this._repository);
@override

View File

@@ -1,19 +1,23 @@
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 '../blocs/client_settings_bloc.dart';
import '../widgets/client_settings_page/settings_actions.dart';
import '../widgets/client_settings_page/settings_profile_header.dart';
import '../widgets/client_settings_page/settings_quick_links.dart';
/// Page for client settings and profile management.
///
/// This page follows the KROW architecture by being a [StatelessWidget]
/// and delegating its state management to [ClientSettingsBloc] and its
/// UI sections to specialized sub-widgets.
class ClientSettingsPage extends StatelessWidget {
/// Creates a [ClientSettingsPage].
const ClientSettingsPage({super.key});
@override
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
return BlocProvider<ClientSettingsBloc>(
create: (context) => Modular.get<ClientSettingsBloc>(),
child: BlocListener<ClientSettingsBloc, ClientSettingsState>(
@@ -27,186 +31,15 @@ class ClientSettingsPage extends StatelessWidget {
).showSnackBar(SnackBar(content: Text(state.message)));
}
},
child: Scaffold(
child: const Scaffold(
backgroundColor: UiColors.background,
body: CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: UiColors.primary,
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [UiColors.primary, Color(0xFF0047FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: UiConstants.space5 * 2,
), // Adjust for SafeArea
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: UiColors.white.withValues(alpha: 0.24),
width: 4,
),
color: UiColors.white,
),
child: const Center(
child: Text(
'C',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: UiColors.primary,
),
),
),
),
const SizedBox(height: UiConstants.space3),
Text(
'Your Company',
style: UiTypography.body1b.copyWith(
color: UiColors.white,
),
),
const SizedBox(height: UiConstants.space1),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
UiIcons.mail,
size: 14,
color: UiColors.white.withValues(alpha: 0.7),
),
const SizedBox(width: UiConstants.space2),
Text(
'client@example.com',
style: UiTypography.footnote1r.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
),
),
SettingsProfileHeader(),
SettingsActions(),
SettingsQuickLinks(),
],
),
],
),
),
),
leading: IconButton(
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
onPressed: () => Modular.to.pop(),
),
title: Text(
labels.title,
style: UiTypography.body1b.copyWith(color: UiColors.white),
),
),
SliverPadding(
padding: const EdgeInsets.all(UiConstants.space5),
sliver: SliverList(
delegate: SliverChildListDelegate([
UiButton.primary(
text: labels.edit_profile,
onPressed: () {},
),
const SizedBox(height: UiConstants.space4),
UiButton.primary(text: labels.hubs, onPressed: () {}),
const SizedBox(height: UiConstants.space4),
BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
builder: (context, state) {
return UiButton.secondary(
text: labels.log_out,
onPressed: state is ClientSettingsLoading
? null
: () => BlocProvider.of<ClientSettingsBloc>(
context,
).add(const ClientSettingsSignOutRequested()),
);
},
),
const SizedBox(height: UiConstants.space5),
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusLg,
side: const BorderSide(color: UiColors.border),
),
color: UiColors.white,
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
labels.quick_links,
style: UiTypography.footnote1b.textPrimary,
),
const SizedBox(height: UiConstants.space3),
_buildQuickLink(
context,
icon: UiIcons.nfc,
title: labels.clock_in_hubs,
onTap: () {},
),
_buildQuickLink(
context,
icon: UiIcons.building,
title: labels.billing_payments,
onTap: () {},
),
],
),
),
),
]),
),
),
],
),
),
),
);
}
Widget _buildQuickLink(
BuildContext context, {
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: UiConstants.radiusMd,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space3,
horizontal: UiConstants.space2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, size: 20, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(title, style: UiTypography.footnote1m.textPrimary),
],
),
const Icon(
UiIcons.chevronRight,
size: 20,
color: UiColors.iconThird,
),
],
),
),
);

View File

@@ -0,0 +1,41 @@
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 '../../blocs/client_settings_bloc.dart';
/// A widget that displays the primary actions for the settings page.
class SettingsActions extends StatelessWidget {
/// Creates a [SettingsActions].
const SettingsActions({super.key});
@override
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
sliver: SliverList(
delegate: SliverChildListDelegate([
const SizedBox(height: UiConstants.space5),
UiButton.primary(text: labels.edit_profile, onPressed: () {}),
const SizedBox(height: UiConstants.space4),
UiButton.primary(text: labels.hubs, onPressed: () {}),
const SizedBox(height: UiConstants.space4),
BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
builder: (context, state) {
return UiButton.secondary(
text: labels.log_out,
onPressed: state is ClientSettingsLoading
? null
: () => BlocProvider.of<ClientSettingsBloc>(
context,
).add(const ClientSettingsSignOutRequested()),
);
},
),
]),
),
);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
/// A widget that displays the profile header with avatar and company info.
class SettingsProfileHeader extends StatelessWidget {
/// Creates a [SettingsProfileHeader].
const SettingsProfileHeader({super.key});
@override
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
return SliverAppBar(
backgroundColor: UiColors.primary,
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [UiColors.primary, Color(0xFF0047FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: UiConstants.space5 * 2),
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: UiColors.white.withValues(alpha: 0.24),
width: 4,
),
color: UiColors.white,
),
child: const Center(
child: Text(
'C',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: UiColors.primary,
),
),
),
),
const SizedBox(height: UiConstants.space3),
Text(
'Your Company',
style: UiTypography.body1b.copyWith(color: UiColors.white),
),
const SizedBox(height: UiConstants.space1),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
UiIcons.mail,
size: 14,
color: UiColors.white.withValues(alpha: 0.7),
),
const SizedBox(width: UiConstants.space2),
Text(
'client@example.com',
style: UiTypography.footnote1r.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
),
),
],
),
],
),
),
),
leading: IconButton(
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
onPressed: () => Modular.to.pop(),
),
title: Text(
labels.title,
style: UiTypography.body1b.copyWith(color: UiColors.white),
),
);
}
}

View File

@@ -0,0 +1,95 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A widget that displays a list of quick links in a card.
class SettingsQuickLinks extends StatelessWidget {
/// Creates a [SettingsQuickLinks].
const SettingsQuickLinks({super.key});
@override
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
return SliverPadding(
padding: const EdgeInsets.all(UiConstants.space5),
sliver: SliverToBoxAdapter(
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusLg,
side: const BorderSide(color: UiColors.border),
),
color: UiColors.white,
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
labels.quick_links,
style: UiTypography.footnote1b.textPrimary,
),
const SizedBox(height: UiConstants.space3),
_QuickLinkItem(
icon: UiIcons.nfc,
title: labels.clock_in_hubs,
onTap: () {},
),
_QuickLinkItem(
icon: UiIcons.building,
title: labels.billing_payments,
onTap: () {},
),
],
),
),
),
),
);
}
}
/// Internal widget for a single quick link item.
class _QuickLinkItem extends StatelessWidget {
final IconData icon;
final String title;
final VoidCallback onTap;
const _QuickLinkItem({
required this.icon,
required this.title,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: UiConstants.radiusMd,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space3,
horizontal: UiConstants.space2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, size: 20, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(title, style: UiTypography.footnote1m.textPrimary),
],
),
const Icon(
UiIcons.chevronRight,
size: 20,
color: UiColors.iconThird,
),
],
),
),
);
}
}