From e321e9a419c91f86e4a53e1a77c04b1a0ac1e65e Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 17 Oct 2025 18:06:32 +0700 Subject: [PATCH] aaa --- lib/app.dart | 23 +- lib/core/router/app_router.dart | 205 +++++++++ .../home/presentation/pages/home_page.dart | 435 +++++++++--------- .../widgets/promotion_slider.dart | 2 +- pubspec.lock | 8 + pubspec.yaml | 3 + 6 files changed, 445 insertions(+), 231 deletions(-) create mode 100644 lib/core/router/app_router.dart diff --git a/lib/app.dart b/lib/app.dart index 931f2e3..3b12f3b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/app_theme.dart'; -import 'package:worker/features/home/presentation/pages/home_page.dart'; import 'package:worker/generated/l10n/app_localizations.dart'; /// Root application widget for Worker Mobile App @@ -19,11 +19,15 @@ class WorkerApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp( + return MaterialApp.router( // ==================== App Configuration ==================== debugShowCheckedModeBanner: false, title: 'Worker App', + // ==================== Router Configuration ==================== + // Using go_router for declarative routing with deep linking support + routerConfig: AppRouter.router, + // ==================== Theme Configuration ==================== // Material 3 theme with brand colors (Primary Blue: #005B9A) theme: AppTheme.lightTheme(), @@ -64,21 +68,6 @@ class WorkerApp extends ConsumerWidget { return const Locale('vi', 'VN'); }, - // ==================== Navigation Configuration ==================== - // TODO: Replace with actual router configuration when navigation is implemented - // Options: - // 1. Use go_router for declarative routing - // 2. Use Navigator 2.0 for imperative routing - // 3. Use auto_route for type-safe routing - // - // For now, we show the home screen directly - home: const HomePage(), - - // Alternative: Use onGenerateRoute for custom routing - // onGenerateRoute: (settings) { - // return AppRouter.onGenerateRoute(settings); - // }, - // ==================== Material App Configuration ==================== // Builder for additional context-dependent widgets builder: (context, child) { diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart new file mode 100644 index 0000000..0b9200b --- /dev/null +++ b/lib/core/router/app_router.dart @@ -0,0 +1,205 @@ +/// App Router Configuration +/// +/// Centralized routing configuration using go_router. +/// Defines all routes, navigation guards, and deep linking. +library; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:worker/features/home/presentation/pages/home_page.dart'; + +/// App Router +/// +/// Handles navigation throughout the app using declarative routing. +/// Features: +/// - Named routes for type-safe navigation +/// - Authentication guards (TODO: implement when auth is ready) +/// - Deep linking support +/// - Transition animations +class AppRouter { + /// Router configuration + static final GoRouter router = GoRouter( + // Initial route + initialLocation: RouteNames.home, + + // Route definitions + routes: [ + // Home Route + GoRoute( + path: RouteNames.home, + name: RouteNames.home, + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + child: const HomePage(), + ), + ), + + // TODO: Add more routes as features are implemented + // Example: + // GoRoute( + // path: RouteNames.products, + // name: RouteNames.products, + // pageBuilder: (context, state) => MaterialPage( + // key: state.pageKey, + // child: const ProductsPage(), + // ), + // ), + ], + + // Error page for unknown routes + errorPageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + child: Scaffold( + appBar: AppBar( + title: const Text('Không tìm thấy trang'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Colors.red, + ), + const SizedBox(height: 16), + const Text( + 'Trang không tồn tại', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + state.uri.toString(), + style: const TextStyle(color: Colors.grey), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => context.go(RouteNames.home), + icon: const Icon(Icons.home), + label: const Text('Về trang chủ'), + ), + ], + ), + ), + ), + ), + + // Redirect logic for authentication (TODO: implement when auth is ready) + // redirect: (context, state) { + // final isLoggedIn = false; // TODO: Get from auth provider + // final isOnLoginPage = state.matchedLocation == RouteNames.login; + // + // if (!isLoggedIn && !isOnLoginPage) { + // return RouteNames.login; + // } + // + // if (isLoggedIn && isOnLoginPage) { + // return RouteNames.home; + // } + // + // return null; + // }, + + // Debug logging (disable in production) + debugLogDiagnostics: true, + ); +} + +/// Route Names +/// +/// Centralized route name constants for type-safe navigation. +/// Use these constants instead of hardcoded strings. +/// +/// Example: +/// ```dart +/// context.go(RouteNames.home); +/// context.push(RouteNames.products); +/// ``` +class RouteNames { + // Private constructor to prevent instantiation + RouteNames._(); + + // Main Routes + static const String home = '/'; + static const String products = '/products'; + static const String productDetail = '/products/:id'; + static const String cart = '/cart'; + static const String checkout = '/checkout'; + static const String orderSuccess = '/order-success'; + + // Loyalty Routes + static const String loyalty = '/loyalty'; + static const String rewards = '/loyalty/rewards'; + static const String pointsHistory = '/loyalty/points-history'; + static const String myGifts = '/loyalty/gifts'; + static const String referral = '/loyalty/referral'; + + // Orders & Payments Routes + static const String orders = '/orders'; + static const String orderDetail = '/orders/:id'; + static const String payments = '/payments'; + + // Projects & Quotes Routes + static const String projects = '/projects'; + static const String projectDetail = '/projects/:id'; + static const String projectCreate = '/projects/create'; + static const String quotes = '/quotes'; + static const String quoteDetail = '/quotes/:id'; + static const String quoteCreate = '/quotes/create'; + + // Account Routes + static const String account = '/account'; + static const String profile = '/account/profile'; + static const String addresses = '/account/addresses'; + static const String addressForm = '/account/addresses/form'; + static const String changePassword = '/account/change-password'; + static const String settings = '/account/settings'; + + // Promotions & Notifications Routes + static const String promotions = '/promotions'; + static const String promotionDetail = '/promotions/:id'; + static const String notifications = '/notifications'; + + // Chat Route + static const String chat = '/chat'; + + // Authentication Routes (TODO: implement when auth feature is ready) + static const String login = '/login'; + static const String otpVerification = '/otp-verification'; + static const String register = '/register'; +} + +/// Route Extensions +/// +/// Helper extensions for common navigation patterns. +extension GoRouterExtension on BuildContext { + /// Navigate to home page + void goHome() => go(RouteNames.home); + + /// Navigate to products page + void goProducts() => go(RouteNames.products); + + /// Navigate to cart page + void goCart() => go(RouteNames.cart); + + /// Navigate to loyalty page + void goLoyalty() => go(RouteNames.loyalty); + + /// Navigate to orders page + void goOrders() => go(RouteNames.orders); + + /// Navigate to projects page + void goProjects() => go(RouteNames.projects); + + /// Navigate to account page + void goAccount() => go(RouteNames.account); + + /// Navigate to promotions page + void goPromotions() => go(RouteNames.promotions); + + /// Navigate to notifications page + void goNotifications() => go(RouteNames.notifications); + + /// Navigate to chat page + void goChat() => go(RouteNames.chat); +} diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index c394c02..b649744 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -12,6 +12,7 @@ import 'package:worker/features/home/presentation/providers/promotions_provider. import 'package:worker/features/home/presentation/widgets/member_card_widget.dart'; import 'package:worker/features/home/presentation/widgets/promotion_slider.dart'; import 'package:worker/features/home/presentation/widgets/quick_action_section.dart'; +import 'package:worker/generated/l10n/app_localizations.dart'; /// Home Page /// @@ -27,6 +28,8 @@ class HomePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + // Watch member card state final memberCardAsync = ref.watch(memberCardProvider); @@ -34,224 +37,230 @@ class HomePage extends ConsumerWidget { final promotionsAsync = ref.watch(promotionsProvider); return Scaffold( + extendBodyBehindAppBar: true, // allow body to render behind status bar + backgroundColor: AppColors.grey50, - body: RefreshIndicator( - onRefresh: () async { - // Refresh both member card and promotions - await Future.wait([ - ref.read(memberCardProvider.notifier).refresh(), - ref.read(promotionsProvider.notifier).refresh(), - ]); - }, - child: CustomScrollView( - slivers: [ - // App Bar - const SliverAppBar( - floating: true, - snap: true, - backgroundColor: AppColors.primaryBlue, - title: Text('Trang ch�'), - centerTitle: true, - ), + body: MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: () async { + // Refresh both member card and promotions + await Future.wait([ + ref.read(memberCardProvider.notifier).refresh(), + ref.read(promotionsProvider.notifier).refresh(), + ]); + }, + child: CustomScrollView( + slivers: [ + // App Bar + // SliverAppBar( + // floating: true, + // snap: true, + // backgroundColor: AppColors.primaryBlue, + // title: Text(l10n.home), + // centerTitle: true, + // ), - // Member Card Section - SliverToBoxAdapter( - child: memberCardAsync.when( - data: (memberCard) => MemberCardWidget(memberCard: memberCard), - loading: () => Container( - margin: const EdgeInsets.all(16), - height: 200, - decoration: BoxDecoration( - color: AppColors.grey100, - borderRadius: BorderRadius.circular(16), + // Member Card Section + SliverToBoxAdapter( + child: memberCardAsync.when( + data: (memberCard) => MemberCardWidget(memberCard: memberCard), + loading: () => Container( + margin: const EdgeInsets.all(16), + height: 200, + decoration: BoxDecoration( + color: AppColors.grey100, + borderRadius: BorderRadius.circular(16), + ), + child: const Center( + child: CircularProgressIndicator(), + ), ), - child: const Center( - child: CircularProgressIndicator(), - ), - ), - error: (error, stack) => Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.danger.withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.error_outline, - color: AppColors.danger, - size: 48, - ), - const SizedBox(height: 8), - Text( - 'Kh�ng th� t�i th� th�nh vi�n', - style: TextStyle( + error: (error, stack) => Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.danger.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error_outline, color: AppColors.danger, - fontWeight: FontWeight.w600, + size: 48, ), - ), - const SizedBox(height: 4), - Text( - error.toString(), - style: TextStyle( - color: AppColors.grey500, - fontSize: 12, + const SizedBox(height: 8), + Text( + l10n.error, + style: const TextStyle( + color: AppColors.danger, + fontWeight: FontWeight.w600, + ), ), - textAlign: TextAlign.center, - ), - ], + const SizedBox(height: 4), + Text( + error.toString(), + style: const TextStyle( + color: AppColors.grey500, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + ), ), ), ), - ), - // Promotions Section - SliverToBoxAdapter( - child: promotionsAsync.when( - data: (promotions) => promotions.isNotEmpty - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: PromotionSlider( - promotions: promotions, - onPromotionTap: (promotion) { - // TODO: Navigate to promotion details - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Xem chi ti�t: ${promotion.title}'), - ), - ); - }, - ), - ) - : const SizedBox.shrink(), - loading: () => const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), + // Promotions Section + SliverToBoxAdapter( + child: promotionsAsync.when( + data: (promotions) => promotions.isNotEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: PromotionSlider( + promotions: promotions, + onPromotionTap: (promotion) { + // TODO: Navigate to promotion details + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${l10n.viewDetails}: ${promotion.title}'), + ), + ); + }, + ), + ) + : const SizedBox.shrink(), + loading: () => const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ), + error: (error, stack) => const SizedBox.shrink(), ), - error: (error, stack) => const SizedBox.shrink(), ), - ), - // Quick Action Sections - SliverToBoxAdapter( - child: Column( - children: [ - const SizedBox(height: 8), - // Products & Cart Section - QuickActionSection( - title: 'S�n ph�m & Gi� h�ng', - actions: [ - QuickAction( - icon: Icons.grid_view, - label: 'S�n ph�m', - onTap: () => _showComingSoon(context, 'S�n ph�m'), - ), - QuickAction( - icon: Icons.shopping_cart, - label: 'Gi� h�ng', - badge: '3', - onTap: () => _showComingSoon(context, 'Gi� h�ng'), - ), - QuickAction( - icon: Icons.favorite, - label: 'Y�u th�ch', - onTap: () => _showComingSoon(context, 'Y�u th�ch'), - ), - ], - ), + // Quick Action Sections + SliverToBoxAdapter( + child: Column( + children: [ + const SizedBox(height: 8), + // Products & Cart Section + QuickActionSection( + title: '${l10n.products} & ${l10n.cart}', + actions: [ + QuickAction( + icon: Icons.grid_view, + label: l10n.products, + onTap: () => _showComingSoon(context, l10n.products, l10n), + ), + QuickAction( + icon: Icons.shopping_cart, + label: l10n.cart, + badge: '3', + onTap: () => _showComingSoon(context, l10n.cart, l10n), + ), + QuickAction( + icon: Icons.favorite, + label: 'Yêu thích', + onTap: () => _showComingSoon(context, 'Yêu thích', l10n), + ), + ], + ), - // Loyalty Section - QuickActionSection( - title: 'Kh�ch h�ng th�n thi�t', - actions: [ - QuickAction( - icon: Icons.card_giftcard, - label: '�i qu�', - onTap: () => _showComingSoon(context, '�i qu�'), - ), - QuickAction( - icon: Icons.history, - label: 'L�ch s� i�m', - onTap: () => _showComingSoon(context, 'L�ch s� i�m'), - ), - QuickAction( - icon: Icons.person_add, - label: 'Gi�i thi�u b�n', - onTap: () => _showComingSoon(context, 'Gi�i thi�u b�n'), - ), - ], - ), + // Loyalty Section + QuickActionSection( + title: l10n.loyalty, + actions: [ + QuickAction( + icon: Icons.card_giftcard, + label: l10n.redeemReward, + onTap: () => _showComingSoon(context, l10n.redeemReward, l10n), + ), + QuickAction( + icon: Icons.history, + label: l10n.pointsHistory, + onTap: () => _showComingSoon(context, l10n.pointsHistory, l10n), + ), + QuickAction( + icon: Icons.person_add, + label: l10n.referral, + onTap: () => _showComingSoon(context, l10n.referral, l10n), + ), + ], + ), - // Quote Requests Section - QuickActionSection( - title: 'Y�u c�u b�o gi� & b�o gi�', - actions: [ - QuickAction( - icon: Icons.description, - label: 'Y�u c�u b�o gi�', - onTap: () => _showComingSoon(context, 'Y�u c�u b�o gi�'), - ), - QuickAction( - icon: Icons.receipt_long, - label: 'B�o gi�', - onTap: () => _showComingSoon(context, 'B�o gi�'), - ), - ], - ), + // Quote Requests Section + QuickActionSection( + title: l10n.quotes, + actions: [ + QuickAction( + icon: Icons.description, + label: l10n.quotes, + onTap: () => _showComingSoon(context, l10n.quotes, l10n), + ), + QuickAction( + icon: Icons.receipt_long, + label: l10n.quotes, + onTap: () => _showComingSoon(context, l10n.quotes, l10n), + ), + ], + ), - // Orders & Payments Section - QuickActionSection( - title: '�n h�ng & thanh to�n', - actions: [ - QuickAction( - icon: Icons.inventory_2, - label: '�n h�ng', - onTap: () => _showComingSoon(context, '�n h�ng'), - ), - QuickAction( - icon: Icons.payment, - label: 'Thanh to�n', - onTap: () => _showComingSoon(context, 'Thanh to�n'), - ), - ], - ), + // Orders & Payments Section + QuickActionSection( + title: '${l10n.orders} & ${l10n.payments}', + actions: [ + QuickAction( + icon: Icons.inventory_2, + label: l10n.orders, + onTap: () => _showComingSoon(context, l10n.orders, l10n), + ), + QuickAction( + icon: Icons.payment, + label: l10n.payments, + onTap: () => _showComingSoon(context, l10n.payments, l10n), + ), + ], + ), - // Sample Houses & News Section - QuickActionSection( - title: 'Nh� m�u, d� �n & tin t�c', - actions: [ - QuickAction( - icon: Icons.home_work, - label: 'Nh� m�u', - onTap: () => _showComingSoon(context, 'Nh� m�u'), - ), - QuickAction( - icon: Icons.business, - label: 'ng k� d� �n', - onTap: () => _showComingSoon(context, 'ng k� d� �n'), - ), - QuickAction( - icon: Icons.article, - label: 'Tin t�c', - onTap: () => _showComingSoon(context, 'Tin t�c'), - ), - ], - ), + // Sample Houses & News Section + QuickActionSection( + title: l10n.projects, + actions: [ + QuickAction( + icon: Icons.home_work, + label: 'Nhà mẫu', + onTap: () => _showComingSoon(context, 'Nhà mẫu', l10n), + ), + QuickAction( + icon: Icons.business, + label: l10n.projects, + onTap: () => _showComingSoon(context, l10n.projects, l10n), + ), + QuickAction( + icon: Icons.article, + label: 'Tin tức', + onTap: () => _showComingSoon(context, 'Tin tức', l10n), + ), + ], + ), - // Bottom Padding (for FAB clearance) - const SizedBox(height: 80), - ], + // Bottom Padding (for FAB clearance) + const SizedBox(height: 80), + ], + ), ), - ), - ], + ], + ), ), ), // Floating Action Button (Chat) floatingActionButton: FloatingActionButton( - onPressed: () => _showComingSoon(context, 'Chat'), + onPressed: () => _showComingSoon(context, l10n.chat, l10n), backgroundColor: AppColors.accentCyan, child: const Icon(Icons.chat_bubble), ), @@ -260,51 +269,51 @@ class HomePage extends ConsumerWidget { bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: 0, - items: const [ + items: [ BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Trang ch�', + icon: const Icon(Icons.home), + label: l10n.home, ), BottomNavigationBarItem( - icon: Icon(Icons.loyalty), - label: 'H�i vi�n', + icon: const Icon(Icons.loyalty), + label: l10n.loyalty, ), BottomNavigationBarItem( - icon: Icon(Icons.local_offer), - label: 'Khuy�n m�i', + icon: const Icon(Icons.local_offer), + label: l10n.promotions, ), BottomNavigationBarItem( - icon: Badge( + icon: const Badge( label: Text('5'), child: Icon(Icons.notifications), ), - label: 'Th�ng b�o', + label: l10n.notifications, ), BottomNavigationBarItem( - icon: Icon(Icons.account_circle), - label: 'C�i �t', + icon: const Icon(Icons.account_circle), + label: l10n.settings, ), ], onTap: (index) { // TODO: Implement navigation final labels = [ - 'Trang ch�', - 'H�i vi�n', - 'Khuy�n m�i', - 'Th�ng b�o', - 'C�i �t' + l10n.home, + l10n.loyalty, + l10n.promotions, + l10n.notifications, + l10n.settings, ]; - _showComingSoon(context, labels[index]); + _showComingSoon(context, labels[index], l10n); }, ), ); } /// Show coming soon message - void _showComingSoon(BuildContext context, String feature) { + void _showComingSoon(BuildContext context, String feature, AppLocalizations l10n) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('$feature - S�p ra m�t'), + content: Text('$feature - ${l10n.comingSoon}'), duration: const Duration(seconds: 1), ), ); diff --git a/lib/features/home/presentation/widgets/promotion_slider.dart b/lib/features/home/presentation/widgets/promotion_slider.dart index 8a0a45b..6cfec80 100644 --- a/lib/features/home/presentation/widgets/promotion_slider.dart +++ b/lib/features/home/presentation/widgets/promotion_slider.dart @@ -46,7 +46,7 @@ class PromotionSlider extends StatelessWidget { ), ), SizedBox( - height: 200, + height: 230, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12), diff --git a/pubspec.lock b/pubspec.lock index 8bd0df7..c9153cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -511,6 +511,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9f00ebd..050fbc7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,9 @@ dependencies: path_provider: ^2.1.3 shared_preferences: ^2.2.3 + # Navigation + go_router: ^14.6.2 + # Icons cupertino_icons: ^1.0.8