update favorite, chat
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/favorites/data/models/favorite_model.dart';
|
||||
// TODO: Re-enable when build_runner generates this file successfully
|
||||
// import 'package:worker/hive_registrar.g.dart';
|
||||
|
||||
@@ -109,8 +110,14 @@ class HiveService {
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter');
|
||||
|
||||
// TODO: Register actual model type adapters when models are created
|
||||
// These will be added to the auto-generated registrar when models are created
|
||||
// Register model type adapters manually
|
||||
// FavoriteModel adapter (typeId: 28)
|
||||
if (!Hive.isAdapterRegistered(HiveTypeIds.favoriteModel)) {
|
||||
Hive.registerAdapter(FavoriteModelAdapter());
|
||||
debugPrint('HiveService: ✓ FavoriteModel adapter registered');
|
||||
}
|
||||
|
||||
// TODO: Register other model type adapters when created
|
||||
// Example:
|
||||
// - UserModel (typeId: 0)
|
||||
// - ProductModel (typeId: 1)
|
||||
|
||||
@@ -6,28 +6,30 @@ library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:worker/features/account/presentation/pages/addresses_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/change_password_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
||||
import 'package:worker/features/cart/presentation/pages/cart_page.dart';
|
||||
import 'package:worker/features/cart/presentation/pages/checkout_page.dart';
|
||||
import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
|
||||
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
|
||||
import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
|
||||
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
|
||||
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/order_detail_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/orders_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/payments_page.dart';
|
||||
import 'package:worker/features/price_policy/price_policy.dart';
|
||||
import 'package:worker/features/products/presentation/pages/product_detail_page.dart';
|
||||
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
||||
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
||||
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
|
||||
import 'package:worker/features/price_policy/price_policy.dart';
|
||||
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
|
||||
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/addresses_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/change_password_page.dart';
|
||||
|
||||
/// App Router
|
||||
///
|
||||
@@ -253,6 +255,14 @@ class AppRouter {
|
||||
MaterialPage(key: state.pageKey, child: const ChangePasswordPage()),
|
||||
),
|
||||
|
||||
// Chat List Route
|
||||
GoRoute(
|
||||
path: RouteNames.chat,
|
||||
name: RouteNames.chat,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const ChatListPage()),
|
||||
),
|
||||
|
||||
// TODO: Add more routes as features are implemented
|
||||
],
|
||||
|
||||
|
||||
462
lib/features/chat/presentation/pages/chat_list_page.dart
Normal file
462
lib/features/chat/presentation/pages/chat_list_page.dart
Normal file
@@ -0,0 +1,462 @@
|
||||
/// Page: Chat List Page
|
||||
///
|
||||
/// List of chat conversations following html/chat-list.html design.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Chat List Page
|
||||
///
|
||||
/// Shows list of chat conversations with:
|
||||
/// - Search functionality
|
||||
/// - Conversation items with avatars
|
||||
/// - Unread indicators
|
||||
/// - Online status
|
||||
/// - Load more button
|
||||
class ChatListPage extends ConsumerStatefulWidget {
|
||||
const ChatListPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ChatListPage> createState() => _ChatListPageState();
|
||||
}
|
||||
|
||||
class _ChatListPageState extends ConsumerState<ChatListPage> {
|
||||
bool _isSearchVisible = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleSearch() {
|
||||
setState(() {
|
||||
_isSearchVisible = !_isSearchVisible;
|
||||
if (!_isSearchVisible) {
|
||||
_searchController.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _newConversation() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Chức năng tạo cuộc trò chuyện mới sẽ được triển khai trong phiên bản tiếp theo.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openChat(String conversationId) {
|
||||
// TODO: Navigate to chat detail page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Mở cuộc trò chuyện: $conversationId')),
|
||||
);
|
||||
}
|
||||
|
||||
void _loadMore() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Đang tải thêm cuộc trò chuyện...')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grey50,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
centerTitle: false,
|
||||
title: const Text(
|
||||
'Tin nhắn',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isSearchVisible ? Icons.search : Icons.search,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: _toggleSearch,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: Colors.black),
|
||||
onPressed: _newConversation,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search Bar (conditional)
|
||||
if (_isSearchVisible)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
color: AppColors.white,
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Tìm kiếm cuộc trò chuyện...',
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.grey500),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, color: AppColors.grey500),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: AppColors.grey50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Conversations List
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
// Conversation 1 - Order Reference
|
||||
_ConversationItem(
|
||||
avatarIcon: Icons.inventory_2,
|
||||
avatarGradient: const [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
contactName: 'Đơn hàng #SO001234',
|
||||
messageTime: '14:30',
|
||||
lastMessage: 'Đơn hàng đang được giao - Dự kiến đến 16:00',
|
||||
lastMessageIcon: Icons.local_shipping,
|
||||
contactType: 'Về: Đơn hàng #SO001234',
|
||||
lastSeen: 'Cập nhật mới',
|
||||
isUnread: true,
|
||||
unreadCount: 2,
|
||||
isOnline: true,
|
||||
onTap: () => _openChat('order001'),
|
||||
),
|
||||
|
||||
// Conversation 2 - Product Reference
|
||||
_ConversationItem(
|
||||
avatarIcon: Icons.inventory_outlined,
|
||||
avatarGradient: null,
|
||||
avatarBackgroundColor: AppColors.grey50,
|
||||
iconColor: AppColors.primaryBlue,
|
||||
contactName: 'Sản phẩm PR0123',
|
||||
messageTime: '12:20',
|
||||
lastMessage: 'Thông tin bổ sung về gạch Granite 60x60',
|
||||
lastMessageIcon: Icons.info_outline,
|
||||
contactType: 'Đơn hàng #DH001233',
|
||||
lastSeen: '2 giờ trước',
|
||||
isUnread: true,
|
||||
unreadCount: 1,
|
||||
isOnline: false,
|
||||
isAway: true,
|
||||
onTap: () => _openChat('product001'),
|
||||
),
|
||||
|
||||
// Conversation 3 - Support Team
|
||||
_ConversationItem(
|
||||
avatarIcon: Icons.headset_mic,
|
||||
avatarGradient: const [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
contactName: 'Tổng đài hỗ trợ',
|
||||
messageTime: '13:45',
|
||||
lastMessage: 'Thông tin về quy trình đổi trả sản phẩm',
|
||||
contactType: 'Bộ phận hỗ trợ',
|
||||
lastSeen: 'Đang hoạt động',
|
||||
isUnread: false,
|
||||
isOnline: true,
|
||||
onTap: () => _openChat('support001'),
|
||||
),
|
||||
|
||||
// Load More Button
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: AppColors.white,
|
||||
child: Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _loadMore,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(color: AppColors.grey100),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.expand_more, size: 20),
|
||||
label: const Text(
|
||||
'Tải thêm cuộc trò chuyện',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversation Item Widget
|
||||
class _ConversationItem extends StatelessWidget {
|
||||
const _ConversationItem({
|
||||
required this.avatarIcon,
|
||||
this.avatarGradient,
|
||||
this.avatarBackgroundColor,
|
||||
this.iconColor,
|
||||
required this.contactName,
|
||||
required this.messageTime,
|
||||
required this.lastMessage,
|
||||
this.lastMessageIcon,
|
||||
required this.contactType,
|
||||
required this.lastSeen,
|
||||
this.isUnread = false,
|
||||
this.unreadCount,
|
||||
this.isOnline = false,
|
||||
this.isAway = false,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData avatarIcon;
|
||||
final List<Color>? avatarGradient;
|
||||
final Color? avatarBackgroundColor;
|
||||
final Color? iconColor;
|
||||
final String contactName;
|
||||
final String messageTime;
|
||||
final String lastMessage;
|
||||
final IconData? lastMessageIcon;
|
||||
final String contactType;
|
||||
final String lastSeen;
|
||||
final bool isUnread;
|
||||
final int? unreadCount;
|
||||
final bool isOnline;
|
||||
final bool isAway;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isUnread
|
||||
? const Color(0xFFF0F9FF) // Light blue background for unread
|
||||
: AppColors.white,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: AppColors.grey100, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Avatar with online indicator
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
gradient: avatarGradient != null
|
||||
? LinearGradient(
|
||||
colors: avatarGradient!,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)
|
||||
: null,
|
||||
color: avatarBackgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
avatarIcon,
|
||||
color: iconColor ?? AppColors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
// Online indicator
|
||||
Positioned(
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: isOnline
|
||||
? AppColors.success
|
||||
: isAway
|
||||
? AppColors.warning
|
||||
: AppColors.grey500,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.white, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Conversation content
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header: Contact name and time
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
contactName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
messageTime,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Preview: Last message and unread count
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (lastMessageIcon != null) ...[
|
||||
Icon(
|
||||
lastMessageIcon,
|
||||
size: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
lastMessage,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (unreadCount != null && unreadCount! > 0) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
child: Text(
|
||||
'$unreadCount',
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Meta: Contact type and last seen
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
contactType,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Text(
|
||||
'•',
|
||||
style: TextStyle(
|
||||
color: AppColors.grey100,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
lastSeen,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,7 @@ class HomePage extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/account/presentation/pages/account_page.dart';
|
||||
import 'package:worker/features/home/presentation/pages/home_page.dart';
|
||||
@@ -40,25 +42,15 @@ class MainScaffold extends ConsumerWidget {
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
index: currentIndex,
|
||||
children: pages,
|
||||
),
|
||||
floatingActionButton: currentIndex == 0
|
||||
body: IndexedStack(index: currentIndex, children: pages),
|
||||
floatingActionButton: currentIndex < 4
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chat - Đang phát triển'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: AppColors.accentCyan,
|
||||
elevation: 8,
|
||||
child: const Icon(Icons.chat_bubble, size: 24, color: Colors.white),
|
||||
onPressed: () => context.push(RouteNames.chat),
|
||||
backgroundColor: const Color(0xFF35C6F4), // Accent cyan color
|
||||
elevation: 4,
|
||||
child: const Icon(Icons.chat_bubble, color: AppColors.white, size: 28),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -66,11 +58,7 @@ class MainScaffold extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -2)),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
@@ -87,18 +75,9 @@ class MainScaffold extends ConsumerWidget {
|
||||
currentIndex: currentIndex,
|
||||
elevation: 0,
|
||||
items: [
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Trang chủ',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.loyalty),
|
||||
label: 'Hội viên',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.local_offer),
|
||||
label: 'Tin tức',
|
||||
),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Trang chủ'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.loyalty), label: 'Hội viên'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.local_offer), label: 'Tin tức'),
|
||||
BottomNavigationBarItem(
|
||||
icon: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
@@ -108,25 +87,12 @@ class MainScaffold extends ConsumerWidget {
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
@@ -135,10 +101,7 @@ class MainScaffold extends ConsumerWidget {
|
||||
),
|
||||
label: 'Thông báo',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.account_circle),
|
||||
label: 'Cài đặt',
|
||||
),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.account_circle), label: 'Cài đặt'),
|
||||
],
|
||||
onTap: (index) {
|
||||
ref.read(currentPageIndexProvider.notifier).setIndex(index);
|
||||
|
||||
@@ -19,10 +19,6 @@ import 'package:worker/generated/l10n/app_localizations.dart';
|
||||
/// Displays product information in a card format.
|
||||
/// Includes image, name, price, stock status, favorite toggle, and add to cart button.
|
||||
class ProductCard extends ConsumerWidget {
|
||||
final Product product;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onAddToCart;
|
||||
|
||||
const ProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
@@ -30,6 +26,10 @@ class ProductCard extends ConsumerWidget {
|
||||
this.onAddToCart,
|
||||
});
|
||||
|
||||
final Product product;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onAddToCart;
|
||||
|
||||
String _formatPrice(double price) {
|
||||
final formatter = NumberFormat('#,###', 'vi_VN');
|
||||
return '${formatter.format(price)}đ';
|
||||
@@ -37,7 +37,7 @@ class ProductCard extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final isFavorited = ref.watch(isFavoriteProvider(product.productId));
|
||||
|
||||
return Card(
|
||||
@@ -142,16 +142,21 @@ class ProductCard extends ConsumerWidget {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
ref
|
||||
onTap: () async {
|
||||
// Capture current state before toggle
|
||||
final wasfavorited = isFavorited;
|
||||
|
||||
// Toggle favorite
|
||||
await ref
|
||||
.read(favoritesProvider.notifier)
|
||||
.toggleFavorite(product.productId);
|
||||
|
||||
// Show feedback
|
||||
// Show feedback with correct message
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
isFavorited
|
||||
wasfavorited
|
||||
? 'Đã xóa khỏi yêu thích'
|
||||
: 'Đã thêm vào yêu thích',
|
||||
),
|
||||
@@ -159,6 +164,7 @@ class ProductCard extends ConsumerWidget {
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
@@ -169,7 +175,7 @@ class ProductCard extends ConsumerWidget {
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user