feat(kell-web): add shell routing and inventory vertical slice
Validate Docs / validate-docs (push) Successful in 53s
Details
Validate Docs / validate-docs (push) Successful in 53s
Details
This commit is contained in:
parent
417430d996
commit
c7c12b3b0d
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'routing/app_routes.dart';
|
||||
|
||||
class KellWebApp extends StatelessWidget {
|
||||
const KellWebApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Kell Creations',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildKcTheme(),
|
||||
initialRoute: AppRoutes.inventory,
|
||||
onGenerateRoute: AppRoutes.onGenerateRoute,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,6 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const KellWebApp());
|
||||
}
|
||||
|
||||
class KellWebApp extends StatelessWidget {
|
||||
const KellWebApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Kell Creations',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildKcTheme(),
|
||||
home: const InventoryPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class DashboardPlaceholderPage extends StatelessWidget {
|
||||
const DashboardPlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Dashboard page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FinancePlaceholderPage extends StatelessWidget {
|
||||
const FinancePlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Finance page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class IntegrationsPlaceholderPage extends StatelessWidget {
|
||||
const IntegrationsPlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Integrations page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class OrdersPlaceholderPage extends StatelessWidget {
|
||||
const OrdersPlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Orders page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class PolicyPlaceholderPage extends StatelessWidget {
|
||||
const PolicyPlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Policy page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProductsPlaceholderPage extends StatelessWidget {
|
||||
const ProductsPlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Products page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../pages/dashboard_placeholder_page.dart';
|
||||
import '../pages/finance_placeholder_page.dart';
|
||||
import '../pages/integrations_placeholder_page.dart';
|
||||
import '../pages/orders_placeholder_page.dart';
|
||||
import '../pages/policy_placeholder_page.dart';
|
||||
import '../pages/products_placeholder_page.dart';
|
||||
import '../shell/app_shell.dart';
|
||||
|
||||
abstract final class AppRoutes {
|
||||
static const String dashboard = '/';
|
||||
static const String inventory = '/inventory';
|
||||
static const String products = '/products';
|
||||
static const String orders = '/orders';
|
||||
static const String finance = '/finance';
|
||||
static const String policy = '/policy';
|
||||
static const String integrations = '/integrations';
|
||||
|
||||
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case dashboard:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(
|
||||
selectedRoute: dashboard,
|
||||
title: 'Dashboard',
|
||||
child: DashboardPlaceholderPage(),
|
||||
),
|
||||
);
|
||||
case inventory:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
|
||||
);
|
||||
case products:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(
|
||||
selectedRoute: products,
|
||||
title: 'Products',
|
||||
child: ProductsPlaceholderPage(),
|
||||
),
|
||||
);
|
||||
case orders:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: orders, title: 'Orders', child: OrdersPlaceholderPage()),
|
||||
);
|
||||
case finance:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: finance, title: 'Finance', child: FinancePlaceholderPage()),
|
||||
);
|
||||
case policy:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: policy, title: 'Policy', child: PolicyPlaceholderPage()),
|
||||
);
|
||||
case integrations:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(
|
||||
selectedRoute: integrations,
|
||||
title: 'Integrations',
|
||||
child: IntegrationsPlaceholderPage(),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static MaterialPageRoute<dynamic> _buildRoute(RouteSettings settings, Widget page) {
|
||||
return MaterialPageRoute<dynamic>(settings: settings, builder: (_) => page);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../routing/app_routes.dart';
|
||||
|
||||
class AppShell extends StatelessWidget {
|
||||
final String selectedRoute;
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const AppShell({
|
||||
super.key,
|
||||
required this.selectedRoute,
|
||||
required this.title,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final index = _routeToIndex(selectedRoute);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Kell Creations')),
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
selectedIndex: index,
|
||||
onDestinationSelected: (selectedIndex) {
|
||||
final route = _indexToRoute(selectedIndex);
|
||||
if (route != selectedRoute) {
|
||||
Navigator.of(context).pushReplacementNamed(route);
|
||||
}
|
||||
},
|
||||
labelType: NavigationRailLabelType.all,
|
||||
minWidth: 88,
|
||||
minExtendedWidth: 200,
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Icon(Icons.storefront_outlined, size: 32),
|
||||
),
|
||||
destinations: const [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: Text('Dashboard'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
selectedIcon: Icon(Icons.inventory_2),
|
||||
label: Text('Inventory'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.sell_outlined),
|
||||
selectedIcon: Icon(Icons.sell),
|
||||
label: Text('Products'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.receipt_long_outlined),
|
||||
selectedIcon: Icon(Icons.receipt_long),
|
||||
label: Text('Orders'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.attach_money_outlined),
|
||||
selectedIcon: Icon(Icons.attach_money),
|
||||
label: Text('Finance'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.policy_outlined),
|
||||
selectedIcon: Icon(Icons.policy),
|
||||
label: Text('Policy'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.hub_outlined),
|
||||
selectedIcon: Icon(Icons.hub),
|
||||
label: Text('Integrations'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static int _routeToIndex(String route) {
|
||||
switch (route) {
|
||||
case AppRoutes.dashboard:
|
||||
return 0;
|
||||
case AppRoutes.inventory:
|
||||
return 1;
|
||||
case AppRoutes.products:
|
||||
return 2;
|
||||
case AppRoutes.orders:
|
||||
return 3;
|
||||
case AppRoutes.finance:
|
||||
return 4;
|
||||
case AppRoutes.policy:
|
||||
return 5;
|
||||
case AppRoutes.integrations:
|
||||
return 6;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static String _indexToRoute(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return AppRoutes.dashboard;
|
||||
case 1:
|
||||
return AppRoutes.inventory;
|
||||
case 2:
|
||||
return AppRoutes.products;
|
||||
case 3:
|
||||
return AppRoutes.orders;
|
||||
case 4:
|
||||
return AppRoutes.finance;
|
||||
case 5:
|
||||
return AppRoutes.policy;
|
||||
case 6:
|
||||
return AppRoutes.integrations;
|
||||
default:
|
||||
return AppRoutes.inventory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_web/main.dart';
|
||||
import 'package:kell_web/app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('app renders inventory page', (WidgetTester tester) async {
|
||||
testWidgets('app shell loads inventory route', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Kell Creations'), findsOneWidget);
|
||||
expect(find.text('Inventory'), findsOneWidget);
|
||||
expect(find.text('Inventory'), findsWidgets);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/get_inventory_items.dart';
|
||||
import '../application/inventory_controller.dart';
|
||||
import '../data/fake_inventory_repository.dart';
|
||||
|
|
@ -33,54 +34,38 @@ class _InventoryPageState extends State<InventoryPage> {
|
|||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Kell Creations')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(KcSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Inventory', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
Text(
|
||||
'Manage handmade products, stock levels, and readiness.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: KcSpacing.lg),
|
||||
if (controller.isLoading)
|
||||
const Expanded(child: Center(child: CircularProgressIndicator()))
|
||||
else if (controller.error != null)
|
||||
Expanded(child: Center(child: Text('Failed to load inventory data.')))
|
||||
else
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
int crossAxisCount = 1;
|
||||
if (width >= 1200) {
|
||||
crossAxisCount = 3;
|
||||
} else if (width >= 700) {
|
||||
crossAxisCount = 2;
|
||||
}
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
itemCount: controller.items.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: KcSpacing.md,
|
||||
mainAxisSpacing: KcSpacing.md,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return InventoryItemCard(item: controller.items[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.error != null) {
|
||||
return const Center(child: Text('Failed to load inventory data.'));
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
|
||||
int crossAxisCount = 1;
|
||||
if (width >= 1200) {
|
||||
crossAxisCount = 3;
|
||||
} else if (width >= 700) {
|
||||
crossAxisCount = 2;
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
itemCount: controller.items.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: KcSpacing.md,
|
||||
mainAxisSpacing: KcSpacing.md,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return InventoryItemCard(item: controller.items[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue