diff --git a/.claude/agents/flutter-widget-expert.md b/.claude/agents/flutter-widget-expert.md index 2715633..3c94a14 100644 --- a/.claude/agents/flutter-widget-expert.md +++ b/.claude/agents/flutter-widget-expert.md @@ -77,6 +77,8 @@ You are a Flutter widget specialist with deep expertise in: - Use `ListView.builder` for long lists +- Use 'spacing' properties for layout spacing in Column, Row + ## Responsive Design: diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index 1660910..4c76de7 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -39,121 +39,111 @@ class HomePage extends ConsumerWidget { final promotionsAsync = ref.watch(promotionsProvider); return Scaffold( - 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: [ - // Add top padding for status bar - SliverPadding( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), - ), + backgroundColor: const Color(0xFFF4F6F8), // --background-gray from CSS + body: CustomScrollView( + slivers: [ + // Add top padding for status bar + SliverPadding( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + ), - // 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()), + // 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), ), - 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, + child: const Center(child: CircularProgressIndicator()), + ), + 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, + size: 48, + ), + const SizedBox(height: 8), + Text( + l10n.error, + style: const TextStyle( color: AppColors.danger, - size: 48, + fontWeight: FontWeight.w600, ), - const SizedBox(height: 8), - Text( - l10n.error, - style: const TextStyle( - color: AppColors.danger, - fontWeight: FontWeight.w600, - ), + ), + const SizedBox(height: 4), + Text( + error.toString(), + style: const TextStyle( + color: AppColors.grey500, + fontSize: 12, ), - const SizedBox(height: 4), - Text( - error.toString(), - style: const TextStyle( - color: AppColors.grey500, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - ), + 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( - '${l10n.viewDetails}: ${promotion.title}', - ), - ), - ); - }, - ), - ) - : const SizedBox.shrink(), - loading: () => const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ), - error: (error, stack) => const SizedBox.shrink(), + // Promotions Section + SliverToBoxAdapter( + child: promotionsAsync.when( + data: (promotions) => promotions.isNotEmpty + ? 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(), ), + ), - // Quick Action Sections - SliverToBoxAdapter( + // Quick Action Sections + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ - const SizedBox(height: 8), // Products & Cart Section QuickActionSection( - title: '${l10n.products} & ${l10n.cart}', + title: 'Sản phẩm & Giỏ hàng', actions: [ QuickAction( icon: Icons.grid_view, - label: l10n.products, + label: 'Sản phẩm', onTap: () => context.pushNamed(RouteNames.products), ), QuickAction( icon: Icons.shopping_cart, - label: l10n.cart, + label: 'Giỏ hàng', badge: '3', - onTap: () => _showComingSoon(context, l10n.cart, l10n), + onTap: () => _showComingSoon(context, 'Giỏ hàng', l10n), ), QuickAction( icon: Icons.favorite, @@ -166,70 +156,55 @@ class HomePage extends ConsumerWidget { // Loyalty Section QuickActionSection( - title: l10n.loyalty, + title: 'Khách hàng thân thiết', actions: [ QuickAction( - icon: Icons.card_giftcard, - label: l10n.redeemReward, + icon: Icons.add_circle_outline, + label: 'Ghi nhận điểm', onTap: () => - _showComingSoon(context, l10n.redeemReward, l10n), + _showComingSoon(context, 'Ghi nhận điểm', l10n), + ), + QuickAction( + icon: Icons.card_giftcard, + label: 'Đổi quà', + onTap: () => _showComingSoon(context, 'Đổi quà', l10n), ), QuickAction( icon: Icons.history, - label: l10n.pointsHistory, + label: 'Lịch sử điểm', 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: 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), + _showComingSoon(context, 'Lịch sử điểm', l10n), ), ], ), // Orders & Payments Section QuickActionSection( - title: '${l10n.orders} & ${l10n.payments}', + title: 'Đơn hàng & thanh toán', actions: [ QuickAction( - icon: Icons.inventory_2, - label: l10n.orders, + icon: Icons.description, + label: 'Yêu cầu báo giá', onTap: () => - _showComingSoon(context, l10n.orders, l10n), + _showComingSoon(context, 'Yêu cầu báo giá', l10n), ), QuickAction( - icon: Icons.payment, - label: l10n.payments, + icon: Icons.inventory_2, + label: 'Đơn hàng', + onTap: () => _showComingSoon(context, 'Đơn hàng', l10n), + ), + QuickAction( + icon: Icons.receipt_long, + label: 'Thanh toán', onTap: () => - _showComingSoon(context, l10n.payments, l10n), + _showComingSoon(context, 'Thanh toán', l10n), ), ], ), // Sample Houses & News Section QuickActionSection( - title: l10n.projects, + title: 'Nhà mẫu, dự án & tin tức', actions: [ QuickAction( icon: Icons.home_work, @@ -238,9 +213,9 @@ class HomePage extends ConsumerWidget { ), QuickAction( icon: Icons.business, - label: l10n.projects, + label: 'Đăng ký dự án', onTap: () => - _showComingSoon(context, l10n.projects, l10n), + _showComingSoon(context, 'Đăng ký dự án', l10n), ), QuickAction( icon: Icons.article, @@ -250,62 +225,119 @@ class HomePage extends ConsumerWidget { ], ), - // Bottom Padding (for FAB clearance) - const SizedBox(height: 80), + // Bottom Padding (for FAB and bottom nav clearance) + const SizedBox(height: 100), ], ), ), - ], + ), + ], + ), + + // Floating Action Button (Chat) - positioned like HTML: bottom: 90px + floatingActionButton: Padding( + padding: const EdgeInsets.only(bottom: 20), // 90px - 70px (bottom nav) + child: FloatingActionButton( + onPressed: () => _showComingSoon(context, l10n.chat, l10n), + backgroundColor: AppColors.accentCyan, + elevation: 8, + child: const Icon(Icons.chat_bubble, size: 24, color: Colors.white), ), ), - // Floating Action Button (Chat) - floatingActionButton: FloatingActionButton( - onPressed: () => _showComingSoon(context, l10n.chat, l10n), - backgroundColor: AppColors.accentCyan, - child: const Icon(Icons.chat_bubble), - ), - // Bottom Navigation Bar - bottomNavigationBar: BottomNavigationBar( - type: BottomNavigationBarType.fixed, - currentIndex: 0, - items: [ - BottomNavigationBarItem( - icon: const Icon(Icons.home), - label: l10n.home, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.loyalty), - label: l10n.loyalty, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.local_offer), - label: l10n.promotions, - ), - BottomNavigationBarItem( - icon: const Badge( - label: Text('5'), - child: Icon(Icons.notifications), + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: SizedBox( + height: 70, + child: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + backgroundColor: Colors.white, + selectedItemColor: AppColors.primaryBlue, + unselectedItemColor: const Color(0xFF666666), + selectedFontSize: 11, + unselectedFontSize: 11, + iconSize: 24, + currentIndex: 0, + elevation: 0, + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.home), + label: 'Trang chủ', + ), + BottomNavigationBarItem( + icon: const Icon(Icons.loyalty), + label: 'Hội viên', + ), + BottomNavigationBarItem( + icon: const Icon(Icons.local_offer), + label: 'Khuyến mãi', + ), + BottomNavigationBarItem( + icon: Stack( + clipBehavior: Clip.none, + children: [ + const Icon(Icons.notifications), + Positioned( + top: -4, + right: -4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppColors.danger, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints( + minWidth: 20, + minHeight: 20, + ), + child: const Text( + '5', + style: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + label: 'Thông báo', + ), + BottomNavigationBarItem( + icon: const Icon(Icons.account_circle), + label: 'Cài đặt', + ), + ], + onTap: (index) { + // TODO: Implement navigation + final labels = [ + 'Trang chủ', + 'Hội viên', + 'Khuyến mãi', + 'Thông báo', + 'Cài đặt', + ]; + _showComingSoon(context, labels[index], l10n); + }, ), - label: l10n.notifications, ), - BottomNavigationBarItem( - icon: const Icon(Icons.account_circle), - label: l10n.settings, - ), - ], - onTap: (index) { - // TODO: Implement navigation - final labels = [ - l10n.home, - l10n.loyalty, - l10n.promotions, - l10n.notifications, - l10n.settings, - ]; - _showComingSoon(context, labels[index], l10n); - }, + ), ), ); } diff --git a/lib/features/home/presentation/widgets/promotion_slider.dart b/lib/features/home/presentation/widgets/promotion_slider.dart index 6cfec80..3755554 100644 --- a/lib/features/home/presentation/widgets/promotion_slider.dart +++ b/lib/features/home/presentation/widgets/promotion_slider.dart @@ -14,17 +14,17 @@ import 'package:worker/features/home/domain/entities/promotion.dart'; /// Displays a horizontal scrollable list of promotion cards. /// Each card shows an image, title, and brief description. class PromotionSlider extends StatelessWidget { - /// List of promotions to display - final List promotions; - - /// Callback when a promotion is tapped - final void Function(Promotion promotion)? onPromotionTap; const PromotionSlider({ super.key, required this.promotions, this.onPromotionTap, }); + /// List of promotions to display + final List promotions; + + /// Callback when a promotion is tapped + final void Function(Promotion promotion)? onPromotionTap; @override Widget build(BuildContext context) { @@ -32,49 +32,54 @@ class PromotionSlider extends StatelessWidget { return const SizedBox.shrink(); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - 'Chương trình ưu đãi', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Chương trình ưu đãi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0xFF212121), // --text-dark + ), ), ), - ), - SizedBox( - height: 230, - child: ListView.builder( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12), - itemCount: promotions.length, - itemBuilder: (context, index) { - return _PromotionCard( - promotion: promotions[index], - onTap: onPromotionTap != null - ? () => onPromotionTap!(promotions[index]) - : null, - ); - }, + const SizedBox(height: 12), + SizedBox( + height: 210, // 140px image + 54px text area + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: promotions.length, + itemBuilder: (context, index) { + return _PromotionCard( + promotion: promotions[index], + onTap: onPromotionTap != null + ? () => onPromotionTap!(promotions[index]) + : null, + ); + }, + ), ), - ), - ], + ], + ), ); } } /// Individual Promotion Card class _PromotionCard extends StatelessWidget { - final Promotion promotion; - final VoidCallback? onTap; const _PromotionCard({ required this.promotion, this.onTap, }); + final Promotion promotion; + final VoidCallback? onTap; @override Widget build(BuildContext context) { @@ -82,13 +87,13 @@ class _PromotionCard extends StatelessWidget { onTap: onTap, child: Container( width: 280, - margin: const EdgeInsets.symmetric(horizontal: 4), + margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2), ), @@ -96,6 +101,7 @@ class _PromotionCard extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ // Promotion Image ClipRRect( @@ -104,7 +110,7 @@ class _PromotionCard extends StatelessWidget { child: CachedNetworkImage( imageUrl: promotion.imageUrl, height: 140, - width: double.infinity, + width: 280, fit: BoxFit.cover, placeholder: (context, url) => Container( height: 140, @@ -126,35 +132,39 @@ class _PromotionCard extends StatelessWidget { ), // Promotion Info - Expanded( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - promotion.title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - promotion.description, - style: TextStyle( - fontSize: 12, - color: AppColors.grey500, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), ), ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + promotion.title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF212121), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + promotion.description, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF666666), // --text-muted + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ], ), diff --git a/lib/features/home/presentation/widgets/quick_action_item.dart b/lib/features/home/presentation/widgets/quick_action_item.dart index 624ff59..2c40579 100644 --- a/lib/features/home/presentation/widgets/quick_action_item.dart +++ b/lib/features/home/presentation/widgets/quick_action_item.dart @@ -34,74 +34,85 @@ class QuickActionItem extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, + return Material( + color: Colors.white, borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Icon with optional badge - Stack( - clipBehavior: Clip.none, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.primaryBlue.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Stack( + alignment: Alignment.center, // Center the column within the stack + clipBehavior: Clip.none, + children: [ + Column( + mainAxisSize: MainAxisSize.max, // Take all available space + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon + Icon( icon, - size: 20, + size: 32, color: AppColors.primaryBlue, ), - ), - // Badge - if (badge != null && badge!.isNotEmpty) - Positioned( - top: -4, - right: -4, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: AppColors.danger, - borderRadius: BorderRadius.circular(10), - ), - constraints: const BoxConstraints( - minWidth: 20, - minHeight: 20, - ), - child: Text( - badge!, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, + const SizedBox(height: 8), + // Label + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF212121), // --text-dark + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + // Badge (positioned absolute like HTML) + if (badge != null && badge!.isNotEmpty) + Positioned( + top: -4, + right: -4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColors.danger, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints( + minWidth: 20, + ), + child: Text( + badge!, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, ), + textAlign: TextAlign.center, ), ), - ], - ), - const SizedBox(height: 8), - // Label - Text( - label, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + ), + ], + ), ), ), ); diff --git a/lib/features/home/presentation/widgets/quick_action_section.dart b/lib/features/home/presentation/widgets/quick_action_section.dart index 9141908..bdd493b 100644 --- a/lib/features/home/presentation/widgets/quick_action_section.dart +++ b/lib/features/home/presentation/widgets/quick_action_section.dart @@ -41,24 +41,41 @@ class QuickActionSection extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.07), + blurRadius: 15, + offset: const Offset(0, 4), + ), + ], + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, children: [ // Section Title Text( title, style: const TextStyle( fontSize: 16, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, + color: Color(0xFF212121), // --text-dark + height: 1.0, // Reduce line height to minimize spacing ), ), - const SizedBox(height: 12), - // Action Grid - _buildActionGrid(), + // Action Grid (always 3 columns to match HTML) + // Using Transform to remove spacing between title and grid + Transform.translate( + offset: const Offset(0, -4), + child: _buildActionGrid(), + ), ], ), ), @@ -66,17 +83,15 @@ class QuickActionSection extends StatelessWidget { } Widget _buildActionGrid() { - // Determine grid layout based on number of items - final int crossAxisCount = actions.length <= 2 ? 2 : 3; - return GridView.builder( + padding: EdgeInsets.zero, // Remove default GridView padding shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, // Always 3 columns to match HTML childAspectRatio: 1.0, - crossAxisSpacing: 4, - mainAxisSpacing: 4, + crossAxisSpacing: 8, + mainAxisSpacing: 8, ), itemCount: actions.length, itemBuilder: (context, index) {