add promotion/detail
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
/// Promotion Detail Page
|
||||
///
|
||||
/// Displays full details of a selected promotion including:
|
||||
/// - Banner image
|
||||
/// - Title and date range
|
||||
/// - Program content
|
||||
/// - Terms and conditions
|
||||
/// - Contact information
|
||||
library;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
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/home/domain/entities/promotion.dart';
|
||||
import 'package:worker/features/home/presentation/providers/promotions_provider.dart';
|
||||
import 'package:worker/features/promotions/presentation/widgets/highlight_box.dart';
|
||||
import 'package:worker/features/promotions/presentation/widgets/promotion_section.dart';
|
||||
|
||||
/// Promotion Detail Page
|
||||
///
|
||||
/// Full-screen detail view of a promotion with scrollable content
|
||||
/// and fixed bottom action bar.
|
||||
class PromotionDetailPage extends ConsumerStatefulWidget {
|
||||
const PromotionDetailPage({
|
||||
this.promotionId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Promotion ID
|
||||
final String? promotionId;
|
||||
|
||||
@override
|
||||
ConsumerState<PromotionDetailPage> createState() =>
|
||||
_PromotionDetailPageState();
|
||||
}
|
||||
|
||||
class _PromotionDetailPageState extends ConsumerState<PromotionDetailPage> {
|
||||
bool _isBookmarked = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch promotions provider
|
||||
final promotionsAsync = ref.watch(promotionsProvider);
|
||||
|
||||
return promotionsAsync.when(
|
||||
data: (promotions) {
|
||||
// Find promotion by ID
|
||||
final promotion = promotions.firstWhere(
|
||||
(p) => p.id == widget.promotionId,
|
||||
orElse: () => promotions.first,
|
||||
);
|
||||
|
||||
return _buildDetailContent(promotion);
|
||||
},
|
||||
loading: () => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Chi tiết khuyến mãi'),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
elevation: 1,
|
||||
),
|
||||
body: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Chi tiết khuyến mãi'),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
elevation: 1,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Không thể tải thông tin khuyến mãi',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailContent(Promotion promotion) {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Scrollable Content
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
// backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
elevation: 1,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.1),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'Chi tiết khuyến mãi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// Share Button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
color: const Color(0xFF64748B),
|
||||
onPressed: _handleShare,
|
||||
),
|
||||
|
||||
// Bookmark Button
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isBookmarked ? Icons.bookmark : Icons.bookmark_border,
|
||||
),
|
||||
color: const Color(0xFF64748B),
|
||||
onPressed: _handleBookmark,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Content
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Banner Image
|
||||
_buildBannerImage(promotion),
|
||||
|
||||
// Promotion Header
|
||||
_buildPromotionHeader(promotion),
|
||||
|
||||
// Program Content Section
|
||||
_buildProgramContentSection(),
|
||||
|
||||
// Terms & Conditions Section
|
||||
_buildTermsSection(),
|
||||
|
||||
// Contact Info Section
|
||||
_buildContactSection(),
|
||||
|
||||
// Bottom padding for action bar
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Fixed Bottom Action Bar
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: _buildActionBar(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build banner image section
|
||||
Widget _buildBannerImage(Promotion promotion) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: promotion.imageUrl,
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
height: 200,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
height: 200,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 64,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build promotion header with title and date
|
||||
Widget _buildPromotionHeader(Promotion promotion) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xFFE2E8F0),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title
|
||||
Text(
|
||||
promotion.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF1E293B),
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Date Range and Status
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
// Clock icon and date
|
||||
const Icon(
|
||||
Icons.access_time,
|
||||
size: 18,
|
||||
color: Color(0xFFF59E0B), // warning color
|
||||
),
|
||||
Text(
|
||||
_formatDateRange(promotion),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFFF59E0B),
|
||||
),
|
||||
),
|
||||
|
||||
// Status Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981), // success color
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_fire_department,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'Đang diễn ra',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build program content section
|
||||
Widget _buildProgramContentSection() {
|
||||
return const PromotionSection(
|
||||
title: 'Nội dung chương trình',
|
||||
icon: Icons.card_giftcard,
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PromotionContentText(
|
||||
'Chương trình khuyến mãi đặc biệt dành cho các công trình xây dựng với mức giảm giá hấp dẫn nhất trong năm.',
|
||||
),
|
||||
|
||||
// Highlight Box
|
||||
HighlightBox(
|
||||
emoji: '🎉',
|
||||
text: 'Giảm giá lên đến 30% cho tất cả sản phẩm gạch men cao cấp',
|
||||
),
|
||||
|
||||
// Discount Details
|
||||
PromotionContentText(
|
||||
'Ưu đãi chi tiết:',
|
||||
isBold: true,
|
||||
),
|
||||
PromotionBulletList(
|
||||
items: [
|
||||
'Gạch men 60x60cm: Giảm 25% - 30%',
|
||||
'Gạch men 80x80cm: Giảm 20% - 25%',
|
||||
'Gạch men 120x60cm: Giảm 15% - 20%',
|
||||
'Gạch granite 60x60cm: Giảm 20% - 25%',
|
||||
'Gạch ốp tường: Giảm 15% - 20%',
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Additional Benefits
|
||||
PromotionContentText(
|
||||
'Ưu đãi bổ sung:',
|
||||
isBold: true,
|
||||
),
|
||||
PromotionBulletList(
|
||||
items: [
|
||||
'Miễn phí vận chuyển cho đơn hàng từ 500m²',
|
||||
'Tặng keo dán gạch cho đơn hàng từ 200m²',
|
||||
'Hỗ trợ thiết kế 3D miễn phí',
|
||||
'Bảo hành sản phẩm lên đến 15 năm',
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build terms and conditions section
|
||||
Widget _buildTermsSection() {
|
||||
return const PromotionSection(
|
||||
title: 'Điều kiện áp dụng',
|
||||
icon: Icons.description,
|
||||
content: PromotionBulletList(
|
||||
items: [
|
||||
'Áp dụng cho tất cả khách hàng là thợ xây dựng đã đăng ký tài khoản',
|
||||
'Đơn hàng tối thiểu: 50m² sản phẩm gạch men',
|
||||
'Thanh toán tối thiểu 50% giá trị đơn hàng khi đặt',
|
||||
'Không áp dụng đồng thời với các chương trình khuyến mãi khác',
|
||||
'Giá đã bao gồm VAT, chưa bao gồm phí vận chuyển',
|
||||
'Sản phẩm không áp dụng đổi trả sau khi đã cắt, gia công',
|
||||
'Thời gian giao hàng: 3-7 ngày làm việc tùy theo khu vực',
|
||||
'Khuyến mãi có thể kết thúc sớm nếu hết hàng',
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build contact information section
|
||||
Widget _buildContactSection() {
|
||||
return const PromotionSection(
|
||||
title: 'Thông tin liên hệ',
|
||||
icon: Icons.phone,
|
||||
isLast: true,
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ContactInfo(
|
||||
label: 'Hotline',
|
||||
value: '1900-xxxx (8:00 - 18:00 hàng ngày)',
|
||||
),
|
||||
ContactInfo(
|
||||
label: 'Email',
|
||||
value: 'promotion@company.com',
|
||||
),
|
||||
ContactInfo(
|
||||
label: 'Zalo',
|
||||
value: '0123.456.789',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build fixed bottom action bar
|
||||
Widget _buildActionBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: const Border(
|
||||
top: BorderSide(
|
||||
color: Color(0xFFE2E8F0),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleViewProducts,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.visibility, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Xem sản phẩm áp dụng',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Format date range for display
|
||||
String _formatDateRange(Promotion promotion) {
|
||||
final startDay = promotion.startDate.day.toString().padLeft(2, '0');
|
||||
final startMonth = promotion.startDate.month.toString().padLeft(2, '0');
|
||||
final startYear = promotion.startDate.year;
|
||||
final endDay = promotion.endDate.day.toString().padLeft(2, '0');
|
||||
final endMonth = promotion.endDate.month.toString().padLeft(2, '0');
|
||||
final endYear = promotion.endDate.year;
|
||||
|
||||
return '$startDay/$startMonth/$startYear - $endDay/$endMonth/$endYear';
|
||||
}
|
||||
|
||||
/// Handle share button press
|
||||
void _handleShare() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tính năng chia sẻ đang phát triển'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle bookmark button press
|
||||
void _handleBookmark() {
|
||||
setState(() {
|
||||
_isBookmarked = !_isBookmarked;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_isBookmarked ? 'Đã lưu khuyến mãi' : 'Đã bỏ lưu khuyến mãi',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle view products button press
|
||||
void _handleViewProducts() {
|
||||
// Navigate to products page
|
||||
context.push(RouteNames.products);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user