diff --git a/kell_creations_apps/apps/kell_web/lib/app.dart b/kell_creations_apps/apps/kell_web/lib/app.dart new file mode 100644 index 0000000..2368a83 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/app.dart @@ -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, + ); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/main.dart b/kell_creations_apps/apps/kell_web/lib/main.dart index 9d14739..f8e12c1 100644 --- a/kell_creations_apps/apps/kell_web/lib/main.dart +++ b/kell_creations_apps/apps/kell_web/lib/main.dart @@ -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(), - ); - } -} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/dashboard_placeholder_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/dashboard_placeholder_page.dart new file mode 100644 index 0000000..c144277 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/pages/dashboard_placeholder_page.dart @@ -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')); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/finance_placeholder_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/finance_placeholder_page.dart new file mode 100644 index 0000000..d132797 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/pages/finance_placeholder_page.dart @@ -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')); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/integrations_placeholder_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/integrations_placeholder_page.dart new file mode 100644 index 0000000..5c05217 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/pages/integrations_placeholder_page.dart @@ -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')); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/orders_placeholder_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/orders_placeholder_page.dart new file mode 100644 index 0000000..268cc3e --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/pages/orders_placeholder_page.dart @@ -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')); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/policy_placeholder_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/policy_placeholder_page.dart new file mode 100644 index 0000000..32a1712 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/pages/policy_placeholder_page.dart @@ -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')); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/products_placeholder_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/products_placeholder_page.dart new file mode 100644 index 0000000..3b64885 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/pages/products_placeholder_page.dart @@ -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')); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart new file mode 100644 index 0000000..000da0f --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart @@ -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 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 _buildRoute(RouteSettings settings, Widget page) { + return MaterialPageRoute(settings: settings, builder: (_) => page); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/shell/app_nav_item.dart b/kell_creations_apps/apps/kell_web/lib/shell/app_nav_item.dart new file mode 100644 index 0000000..e69de29 diff --git a/kell_creations_apps/apps/kell_web/lib/shell/app_shell.dart b/kell_creations_apps/apps/kell_web/lib/shell/app_shell.dart new file mode 100644 index 0000000..5c51d76 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/shell/app_shell.dart @@ -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; + } + } +} diff --git a/kell_creations_apps/apps/kell_web/test/widget_test.dart b/kell_creations_apps/apps/kell_web/test/widget_test.dart index 58145d3..c897302 100644 --- a/kell_creations_apps/apps/kell_web/test/widget_test.dart +++ b/kell_creations_apps/apps/kell_web/test/widget_test.dart @@ -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); }); } diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart index c184abd..f4a59d8 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart @@ -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 { 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]); + }, + ); + }, ); }, );