add auth, format
This commit is contained in:
@@ -26,10 +26,7 @@ import 'package:worker/features/products/presentation/widgets/product_detail/sti
|
||||
class ProductDetailPage extends ConsumerStatefulWidget {
|
||||
final String productId;
|
||||
|
||||
const ProductDetailPage({
|
||||
super.key,
|
||||
required this.productId,
|
||||
});
|
||||
const ProductDetailPage({super.key, required this.productId});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductDetailPage> createState() => _ProductDetailPageState();
|
||||
@@ -70,9 +67,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_isFavorite
|
||||
? 'Đã thêm vào yêu thích'
|
||||
: 'Đã xóa khỏi yêu thích',
|
||||
_isFavorite ? 'Đã thêm vào yêu thích' : 'Đã xóa khỏi yêu thích',
|
||||
),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
@@ -109,10 +104,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
// Title
|
||||
const Text(
|
||||
'Chia sẻ sản phẩm',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
@@ -144,9 +136,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
Navigator.pop(context);
|
||||
// TODO: Copy to clipboard
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đã sao chép link sản phẩm!'),
|
||||
),
|
||||
const SnackBar(content: Text('Đã sao chép link sản phẩm!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -269,9 +259,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
child: CircularProgressIndicator(color: AppColors.primaryBlue),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Padding(
|
||||
@@ -287,10 +275,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
const Text(
|
||||
'Không thể tải thông tin sản phẩm',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
@@ -55,7 +55,10 @@ class ProductsPage extends ConsumerWidget {
|
||||
backgroundColor: AppColors.danger,
|
||||
textColor: AppColors.white,
|
||||
isLabelVisible: cartItemCount > 0,
|
||||
child: const Icon(Icons.shopping_cart_outlined, color: Colors.black),
|
||||
child: const Icon(
|
||||
Icons.shopping_cart_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
onPressed: () => context.push(RouteNames.cart),
|
||||
),
|
||||
@@ -74,9 +77,7 @@ class ProductsPage extends ConsumerWidget {
|
||||
data: (categories) => CategoryFilterChips(categories: categories),
|
||||
loading: () => const SizedBox(
|
||||
height: 48.0,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2.0),
|
||||
),
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||
),
|
||||
error: (error, stack) => const SizedBox.shrink(),
|
||||
),
|
||||
@@ -115,7 +116,8 @@ class ProductsPage extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => _buildLoadingState(),
|
||||
error: (error, stack) => _buildErrorState(context, l10n, error, ref),
|
||||
error: (error, stack) =>
|
||||
_buildErrorState(context, l10n, error, ref),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -146,10 +148,7 @@ class ProductsPage extends ConsumerWidget {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
l10n.noResults,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -159,9 +158,7 @@ class ProductsPage extends ConsumerWidget {
|
||||
/// Build loading state
|
||||
Widget _buildLoadingState() {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
child: CircularProgressIndicator(color: AppColors.primaryBlue),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,10 +192,7 @@ class ProductsPage extends ConsumerWidget {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
@@ -17,10 +17,7 @@ import 'package:worker/features/products/presentation/providers/selected_categor
|
||||
class CategoryFilterChips extends ConsumerWidget {
|
||||
final List<Category> categories;
|
||||
|
||||
const CategoryFilterChips({
|
||||
super.key,
|
||||
required this.categories,
|
||||
});
|
||||
const CategoryFilterChips({super.key, required this.categories});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -32,7 +29,8 @@ class CategoryFilterChips extends ConsumerWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
itemCount: categories.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: AppSpacing.sm),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final isSelected = selectedCategory == category.id;
|
||||
@@ -49,7 +47,9 @@ class CategoryFilterChips extends ConsumerWidget {
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
ref.read(selectedCategoryProvider.notifier).updateCategory(category.id);
|
||||
ref
|
||||
.read(selectedCategoryProvider.notifier)
|
||||
.updateCategory(category.id);
|
||||
}
|
||||
},
|
||||
backgroundColor: AppColors.white,
|
||||
|
||||
@@ -70,9 +70,7 @@ class ProductCard extends ConsumerWidget {
|
||||
placeholder: (context, url) => Shimmer.fromColors(
|
||||
baseColor: AppColors.grey100,
|
||||
highlightColor: AppColors.grey50,
|
||||
child: Container(
|
||||
color: AppColors.grey100,
|
||||
),
|
||||
child: Container(color: AppColors.grey100),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: AppColors.grey100,
|
||||
@@ -182,8 +180,12 @@ class ProductCard extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
isFavorited ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFavorited ? AppColors.danger : AppColors.grey500,
|
||||
isFavorited
|
||||
? Icons.favorite
|
||||
: Icons.favorite_border,
|
||||
color: isFavorited
|
||||
? AppColors.danger
|
||||
: AppColors.grey500,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -244,7 +246,9 @@ class ProductCard extends ConsumerWidget {
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppRadius.button,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
@@ -272,7 +276,9 @@ class ProductCard extends ConsumerWidget {
|
||||
// For now, show a message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đang phát triển tính năng xem 360°'),
|
||||
content: Text(
|
||||
'Đang phát triển tính năng xem 360°',
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -285,7 +291,9 @@ class ProductCard extends ConsumerWidget {
|
||||
),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppRadius.button,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
|
||||
@@ -20,10 +20,7 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
||||
class ImageGallerySection extends StatefulWidget {
|
||||
final Product product;
|
||||
|
||||
const ImageGallerySection({
|
||||
super.key,
|
||||
required this.product,
|
||||
});
|
||||
const ImageGallerySection({super.key, required this.product});
|
||||
|
||||
@override
|
||||
State<ImageGallerySection> createState() => _ImageGallerySectionState();
|
||||
@@ -232,9 +229,8 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: images[index],
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
color: AppColors.grey100,
|
||||
),
|
||||
placeholder: (context, url) =>
|
||||
Container(color: AppColors.grey100),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: AppColors.grey100,
|
||||
child: const Icon(
|
||||
@@ -321,10 +317,7 @@ class _ImageLightboxState extends State<_ImageLightbox> {
|
||||
),
|
||||
title: Text(
|
||||
'${_currentIndex + 1} / ${widget.images.length}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
style: const TextStyle(color: AppColors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
@@ -414,18 +407,12 @@ class _ImageLightboxState extends State<_ImageLightbox> {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withAlpha(128),
|
||||
Colors.transparent,
|
||||
],
|
||||
colors: [Colors.black.withAlpha(128), Colors.transparent],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
widget.imageCaptions[widget.images[_currentIndex]] ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
style: const TextStyle(color: AppColors.white, fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -19,10 +19,7 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
||||
class ProductInfoSection extends StatelessWidget {
|
||||
final Product product;
|
||||
|
||||
const ProductInfoSection({
|
||||
super.key,
|
||||
required this.product,
|
||||
});
|
||||
const ProductInfoSection({super.key, required this.product});
|
||||
|
||||
String _formatPrice(double price) {
|
||||
final formatter = NumberFormat('#,###', 'vi_VN');
|
||||
@@ -40,10 +37,7 @@ class ProductInfoSection extends StatelessWidget {
|
||||
// SKU
|
||||
Text(
|
||||
'SKU: ${product.erpnextItemCode ?? product.productId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
@@ -168,18 +162,11 @@ class _QuickInfoCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: AppColors.primaryBlue,
|
||||
size: 24,
|
||||
),
|
||||
Icon(icon, color: AppColors.primaryBlue, size: 24),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
|
||||
@@ -17,10 +17,7 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
||||
class ProductTabsSection extends StatefulWidget {
|
||||
final Product product;
|
||||
|
||||
const ProductTabsSection({
|
||||
super.key,
|
||||
required this.product,
|
||||
});
|
||||
const ProductTabsSection({super.key, required this.product});
|
||||
|
||||
@override
|
||||
State<ProductTabsSection> createState() => _ProductTabsSectionState();
|
||||
@@ -52,10 +49,7 @@ class _ProductTabsSectionState extends State<ProductTabsSection>
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xFFe0e0e0),
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(color: Color(0xFFe0e0e0), width: 1),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
@@ -152,29 +146,31 @@ class _DescriptionTab extends StatelessWidget {
|
||||
'Màu sắc bền đẹp theo thời gian',
|
||||
'Dễ dàng vệ sinh và bảo trì',
|
||||
'Thân thiện với môi trường',
|
||||
].map((feature) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
color: AppColors.success,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
feature,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
].map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
color: AppColors.success,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
feature,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@@ -242,9 +238,7 @@ class _SpecificationsTab extends StatelessWidget {
|
||||
border: isLast
|
||||
? null
|
||||
: const Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xFFe0e0e0),
|
||||
),
|
||||
bottom: BorderSide(color: Color(0xFFe0e0e0)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -335,9 +329,7 @@ class _ReviewsTab extends StatelessWidget {
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) => Icon(
|
||||
index < 4
|
||||
? Icons.star
|
||||
: Icons.star_half,
|
||||
index < 4 ? Icons.star : Icons.star_half,
|
||||
color: const Color(0xFFffc107),
|
||||
size: 18,
|
||||
),
|
||||
@@ -349,10 +341,7 @@ class _ReviewsTab extends StatelessWidget {
|
||||
// Review count
|
||||
const Text(
|
||||
'125 đánh giá',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -382,11 +371,7 @@ class _ReviewItem extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xFFe0e0e0),
|
||||
),
|
||||
),
|
||||
border: Border(bottom: BorderSide(color: Color(0xFFe0e0e0))),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -482,6 +467,7 @@ final _mockReviews = [
|
||||
'name': 'Trần Thị B',
|
||||
'date': '1 tháng trước',
|
||||
'rating': 4,
|
||||
'text': 'Gạch đẹp, vân gỗ rất chân thực. Giao hàng nhanh chóng và đóng gói cẩn thận.',
|
||||
'text':
|
||||
'Gạch đẹp, vân gỗ rất chân thực. Giao hàng nhanh chóng và đóng gói cẩn thận.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,10 +37,7 @@ class StickyActionBar extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: const Color(0xFFe0e0e0),
|
||||
width: 1,
|
||||
),
|
||||
top: BorderSide(color: const Color(0xFFe0e0e0), width: 1),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -58,9 +55,7 @@ class StickyActionBar extends StatelessWidget {
|
||||
// Quantity Controls
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFFe0e0e0),
|
||||
),
|
||||
border: Border.all(color: const Color(0xFFe0e0e0)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
@@ -81,9 +76,7 @@ class StickyActionBar extends StatelessWidget {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -101,10 +94,7 @@ class StickyActionBar extends StatelessWidget {
|
||||
),
|
||||
|
||||
// Increase Button
|
||||
_QuantityButton(
|
||||
icon: Icons.add,
|
||||
onPressed: onIncrease,
|
||||
),
|
||||
_QuantityButton(icon: Icons.add, onPressed: onIncrease),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -151,10 +141,7 @@ class _QuantityButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const _QuantityButton({
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
});
|
||||
const _QuantityButton({required this.icon, this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -104,9 +104,7 @@ class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
),
|
||||
style: const TextStyle(fontSize: InputFieldSpecs.fontSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user