feat: Add client main shell with bottom navigation
Introduces the new client_main feature package, providing a main shell with bottom navigation for the client app. Updates routing to use /client-main instead of /client-home, adds localization for new tabs, and implements navigation logic, UI, and tests for the main shell. Also refactors create_order module bindings and cleans up unused dependencies.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
library;
|
||||
|
||||
export 'src/client_main_module.dart';
|
||||
export 'src/presentation/navigation/client_main_navigator.dart';
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:client_home/client_home.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import 'presentation/blocs/client_main_cubit.dart';
|
||||
import 'presentation/pages/client_main_page.dart';
|
||||
import 'presentation/pages/placeholder_page.dart';
|
||||
|
||||
class ClientMainModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
i.addSingleton(ClientMainCubit.new);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child(
|
||||
'/',
|
||||
child: (BuildContext context) => const ClientMainPage(),
|
||||
children: <ParallelRoute<dynamic>>[
|
||||
ModuleRoute<dynamic>('/home', module: ClientHomeModule()),
|
||||
// Placeholders for other tabs
|
||||
ChildRoute<dynamic>(
|
||||
'/coverage',
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Coverage'),
|
||||
),
|
||||
ChildRoute<dynamic>(
|
||||
'/billing',
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Billing'),
|
||||
),
|
||||
ChildRoute<dynamic>(
|
||||
'/orders',
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Orders'),
|
||||
),
|
||||
ChildRoute<dynamic>(
|
||||
'/reports',
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Reports'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'client_main_state.dart';
|
||||
|
||||
class ClientMainCubit extends Cubit<ClientMainState> implements Disposable {
|
||||
ClientMainCubit() : super(const ClientMainState()) {
|
||||
Modular.to.addListener(_onRouteChanged);
|
||||
_onRouteChanged();
|
||||
}
|
||||
|
||||
void _onRouteChanged() {
|
||||
final String path = Modular.to.path;
|
||||
int newIndex = state.currentIndex;
|
||||
|
||||
// Detect which tab is active based on the route path
|
||||
// Using contains() to handle child routes and trailing slashes
|
||||
if (path.contains('/client-main/coverage')) {
|
||||
newIndex = 0;
|
||||
} else if (path.contains('/client-main/billing')) {
|
||||
newIndex = 1;
|
||||
} else if (path.contains('/client-main/home')) {
|
||||
newIndex = 2;
|
||||
} else if (path.contains('/client-main/orders')) {
|
||||
newIndex = 3;
|
||||
} else if (path.contains('/client-main/reports')) {
|
||||
newIndex = 4;
|
||||
}
|
||||
|
||||
if (newIndex != state.currentIndex) {
|
||||
emit(state.copyWith(currentIndex: newIndex));
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToTab(int index) {
|
||||
if (index == state.currentIndex) return;
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
Modular.to.navigate('/client-main/coverage');
|
||||
break;
|
||||
case 1:
|
||||
Modular.to.navigate('/client-main/billing');
|
||||
break;
|
||||
case 2:
|
||||
Modular.to.navigate('/client-main/home');
|
||||
break;
|
||||
case 3:
|
||||
Modular.to.navigate('/client-main/orders');
|
||||
break;
|
||||
case 4:
|
||||
Modular.to.navigate('/client-main/reports');
|
||||
break;
|
||||
}
|
||||
// State update will happen via _onRouteChanged
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Modular.to.removeListener(_onRouteChanged);
|
||||
close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class ClientMainState extends Equatable {
|
||||
const ClientMainState({
|
||||
this.currentIndex = 2, // Default to Home
|
||||
});
|
||||
|
||||
final int currentIndex;
|
||||
|
||||
ClientMainState copyWith({int? currentIndex}) {
|
||||
return ClientMainState(currentIndex: currentIndex ?? this.currentIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => <Object>[currentIndex];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
/// Extension to provide typed navigation for the Client Main feature.
|
||||
extension ClientMainNavigator on IModularNavigator {
|
||||
/// Navigates to the Client Main Shell (Home).
|
||||
/// This replaces the current navigation stack.
|
||||
void navigateClientMain() {
|
||||
navigate('/client-main/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import '../blocs/client_main_cubit.dart';
|
||||
import '../blocs/client_main_state.dart';
|
||||
import '../widgets/client_main_bottom_bar.dart';
|
||||
|
||||
/// The main page for the Client app, acting as a shell for the bottom navigation.
|
||||
///
|
||||
/// It follows KROW Clean Architecture by:
|
||||
/// - Being a [StatelessWidget].
|
||||
/// - Delegating state management to [ClientMainCubit].
|
||||
/// - Using [RouterOutlet] for nested navigation.
|
||||
class ClientMainPage extends StatelessWidget {
|
||||
const ClientMainPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ClientMainCubit>(
|
||||
create: (BuildContext context) => Modular.get<ClientMainCubit>(),
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
body: const RouterOutlet(),
|
||||
bottomNavigationBar: BlocBuilder<ClientMainCubit, ClientMainState>(
|
||||
builder: (BuildContext context, ClientMainState state) {
|
||||
return ClientMainBottomBar(
|
||||
currentIndex: state.currentIndex,
|
||||
onTap: (int index) {
|
||||
BlocProvider.of<ClientMainCubit>(context).navigateToTab(index);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A placeholder page for features that are not yet implemented.
|
||||
///
|
||||
/// This page displays a simple message indicating that the feature
|
||||
/// is coming soon. It follows the KROW Design System guidelines by:
|
||||
/// - Using [UiAppBar] for the app bar
|
||||
/// - Using [UiTypography] for text styling
|
||||
/// - Using [UiColors] via typography extensions
|
||||
class PlaceholderPage extends StatelessWidget {
|
||||
/// Creates a [PlaceholderPage].
|
||||
///
|
||||
/// The [title] is displayed in the app bar and used in the
|
||||
/// "coming soon" message.
|
||||
const PlaceholderPage({required this.title, super.key});
|
||||
|
||||
/// The title of the feature being displayed.
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(title: title),
|
||||
body: Center(
|
||||
child: Text(
|
||||
'$title Feature Coming Soon',
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A custom bottom navigation bar for the Client app.
|
||||
///
|
||||
/// This widget provides a glassmorphic bottom navigation bar with blur effect
|
||||
/// and follows the KROW Design System guidelines. It displays five tabs:
|
||||
/// Coverage, Billing, Home, Orders, and Reports.
|
||||
///
|
||||
/// The widget uses:
|
||||
/// - [UiColors] for all color values
|
||||
/// - [UiTypography] for text styling
|
||||
/// - [UiIcons] for icon assets
|
||||
/// - [UiConstants] for spacing and sizing
|
||||
class ClientMainBottomBar extends StatelessWidget {
|
||||
/// Creates a [ClientMainBottomBar].
|
||||
///
|
||||
/// The [currentIndex] indicates which tab is currently selected.
|
||||
/// The [onTap] callback is invoked when a tab is tapped.
|
||||
const ClientMainBottomBar({
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The index of the currently selected tab.
|
||||
final int currentIndex;
|
||||
|
||||
/// Callback invoked when a tab is tapped.
|
||||
///
|
||||
/// The callback receives the index of the tapped tab.
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Client App colors from design system
|
||||
const Color activeColor = UiColors.textPrimary;
|
||||
const Color inactiveColor = UiColors.textInactive;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
// Glassmorphic background with blur effect
|
||||
Positioned.fill(
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withValues(alpha: 0.85),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Navigation items
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space2,
|
||||
top: UiConstants.space4,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
_buildNavItem(
|
||||
index: 0,
|
||||
icon: UiIcons.calendar,
|
||||
label: t.client_main.tabs.coverage,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 1,
|
||||
icon: UiIcons.dollar,
|
||||
label: t.client_main.tabs.billing,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 2,
|
||||
icon: UiIcons.building,
|
||||
label: t.client_main.tabs.home,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 3,
|
||||
icon: UiIcons.file,
|
||||
label: t.client_main.tabs.orders,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 4,
|
||||
icon: UiIcons.chart,
|
||||
label: t.client_main.tabs.reports,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a single navigation item.
|
||||
///
|
||||
/// Uses design system tokens for all styling:
|
||||
/// - Icon size uses a standard value (24px is acceptable for navigation icons)
|
||||
/// - Spacing uses [UiConstants.space1]
|
||||
/// - Typography uses [UiTypography.footnote2m]
|
||||
/// - Colors are passed as parameters from design system
|
||||
Widget _buildNavItem({
|
||||
required int index,
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color activeColor,
|
||||
required Color inactiveColor,
|
||||
}) {
|
||||
final bool isSelected = currentIndex == index;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected ? activeColor : inactiveColor,
|
||||
size: 24, // Standard navigation icon size
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: isSelected ? activeColor : inactiveColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user