refactor: extract UI components into dedicated widgets for the client settings page and update repository constructor.
This commit is contained in:
@@ -15,7 +15,7 @@ class ClientSettingsModule extends Module {
|
|||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repositories
|
// Repositories
|
||||||
i.addLazySingleton<SettingsRepositoryInterface>(
|
i.addLazySingleton<SettingsRepositoryInterface>(
|
||||||
() => SettingsRepositoryImpl(i.get<AuthRepositoryMock>()),
|
() => SettingsRepositoryImpl(mock: i.get<AuthRepositoryMock>()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import '../../domain/repositories/settings_repository_interface.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 {
|
class SettingsRepositoryImpl implements SettingsRepositoryInterface {
|
||||||
final AuthRepositoryMock _authMock;
|
/// The auth mock from data connect.
|
||||||
|
final AuthRepositoryMock mock;
|
||||||
|
|
||||||
/// Creates a [SettingsRepositoryImpl].
|
/// Creates a [SettingsRepositoryImpl] with the required [mock].
|
||||||
SettingsRepositoryImpl(this._authMock);
|
SettingsRepositoryImpl({required this.mock});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() {
|
Future<void> signOut() {
|
||||||
return _authMock.signOut();
|
return mock.signOut();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
abstract interface class SettingsRepositoryInterface {
|
||||||
/// Signs out the current user.
|
/// Signs out the current user from the application.
|
||||||
Future<void> signOut();
|
Future<void> signOut();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import 'package:krow_core/core.dart';
|
|||||||
import '../repositories/settings_repository_interface.dart';
|
import '../repositories/settings_repository_interface.dart';
|
||||||
|
|
||||||
/// Use case handles the user sign out process.
|
/// Use case handles the user sign out process.
|
||||||
|
///
|
||||||
|
/// This use case delegates the sign out logic to the [SettingsRepositoryInterface].
|
||||||
class SignOutUseCase implements NoInputUseCase<void> {
|
class SignOutUseCase implements NoInputUseCase<void> {
|
||||||
final SettingsRepositoryInterface _repository;
|
final SettingsRepositoryInterface _repository;
|
||||||
|
|
||||||
/// Creates a [SignOutUseCase].
|
/// Creates a [SignOutUseCase].
|
||||||
|
///
|
||||||
|
/// Requires a [SettingsRepositoryInterface] to perform the sign out operation.
|
||||||
SignOutUseCase(this._repository);
|
SignOutUseCase(this._repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
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:flutter_modular/flutter_modular.dart';
|
||||||
import '../blocs/client_settings_bloc.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.
|
/// 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 {
|
class ClientSettingsPage extends StatelessWidget {
|
||||||
/// Creates a [ClientSettingsPage].
|
/// Creates a [ClientSettingsPage].
|
||||||
const ClientSettingsPage({super.key});
|
const ClientSettingsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final labels = t.client_settings.profile;
|
|
||||||
|
|
||||||
return BlocProvider<ClientSettingsBloc>(
|
return BlocProvider<ClientSettingsBloc>(
|
||||||
create: (context) => Modular.get<ClientSettingsBloc>(),
|
create: (context) => Modular.get<ClientSettingsBloc>(),
|
||||||
child: BlocListener<ClientSettingsBloc, ClientSettingsState>(
|
child: BlocListener<ClientSettingsBloc, ClientSettingsState>(
|
||||||
@@ -27,186 +31,15 @@ class ClientSettingsPage extends StatelessWidget {
|
|||||||
).showSnackBar(SnackBar(content: Text(state.message)));
|
).showSnackBar(SnackBar(content: Text(state.message)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
backgroundColor: UiColors.background,
|
backgroundColor: UiColors.background,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SettingsProfileHeader(),
|
||||||
backgroundColor: UiColors.primary,
|
SettingsActions(),
|
||||||
expandedHeight: 200,
|
SettingsQuickLinks(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user