update
This commit is contained in:
192
lib/features/home/presentation/widgets/member_card_widget.dart
Normal file
192
lib/features/home/presentation/widgets/member_card_widget.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
/// Widget: Member Card Widget
|
||||
///
|
||||
/// Displays a user's membership card with tier-specific styling.
|
||||
/// Shows member information, points, QR code, and tier badge.
|
||||
///
|
||||
/// Supports three tiers: Diamond, Platinum, Gold
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/home/domain/entities/member_card.dart';
|
||||
|
||||
/// Member Card Widget
|
||||
///
|
||||
/// Renders a beautiful gradient card displaying member information.
|
||||
/// The gradient and styling changes based on the membership tier.
|
||||
class MemberCardWidget extends StatelessWidget {
|
||||
/// Member card data
|
||||
final MemberCard memberCard;
|
||||
|
||||
const MemberCardWidget({
|
||||
super.key,
|
||||
required this.memberCard,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
gradient: _getGradientForTier(memberCard.tier),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row: Branding and Valid Until
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Branding
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'EUROTILE',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
memberCard.memberType.displayName,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Valid Until
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'Valid through',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatDate(memberCard.validUntil),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Footer Row: Member Info and QR Code
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Member Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
memberCard.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'CLASS: ${memberCard.tier.displayName}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Points: ${_formatPoints(memberCard.points)}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// QR Code
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: memberCard.qrData,
|
||||
version: QrVersions.auto,
|
||||
size: 60,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get gradient for tier
|
||||
LinearGradient _getGradientForTier(MemberTier tier) {
|
||||
switch (tier) {
|
||||
case MemberTier.diamond:
|
||||
return AppColors.diamondGradient;
|
||||
case MemberTier.platinum:
|
||||
return AppColors.platinumGradient;
|
||||
case MemberTier.gold:
|
||||
return AppColors.goldGradient;
|
||||
}
|
||||
}
|
||||
|
||||
/// Format date to DD/MM/YYYY
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||
}
|
||||
|
||||
/// Format points with thousands separator
|
||||
String _formatPoints(int points) {
|
||||
return points.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]},',
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/features/home/presentation/widgets/promotion_slider.dart
Normal file
164
lib/features/home/presentation/widgets/promotion_slider.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
/// Widget: Promotion Slider
|
||||
///
|
||||
/// Horizontal scrolling list of promotional banners.
|
||||
/// Displays promotion images, titles, and descriptions.
|
||||
library;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||
|
||||
/// Promotion Slider Widget
|
||||
///
|
||||
/// 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<Promotion> promotions;
|
||||
|
||||
/// Callback when a promotion is tapped
|
||||
final void Function(Promotion promotion)? onPromotionTap;
|
||||
|
||||
const PromotionSlider({
|
||||
super.key,
|
||||
required this.promotions,
|
||||
this.onPromotionTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (promotions.isEmpty) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Individual Promotion Card
|
||||
class _PromotionCard extends StatelessWidget {
|
||||
final Promotion promotion;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _PromotionCard({
|
||||
required this.promotion,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 280,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Promotion Image
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: promotion.imageUrl,
|
||||
height: 140,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
height: 140,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
height: 140,
|
||||
color: AppColors.grey100,
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/features/home/presentation/widgets/quick_action_item.dart
Normal file
109
lib/features/home/presentation/widgets/quick_action_item.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
/// Widget: Quick Action Item
|
||||
///
|
||||
/// Individual action button with icon and label.
|
||||
/// Used in quick action grids on the home screen.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Quick Action Item Widget
|
||||
///
|
||||
/// Displays an icon button with a label below.
|
||||
/// Supports optional badge for notifications or counts.
|
||||
class QuickActionItem extends StatelessWidget {
|
||||
/// Icon to display
|
||||
final IconData icon;
|
||||
|
||||
/// Label text
|
||||
final String label;
|
||||
|
||||
/// Optional badge text (e.g., "3" for cart items)
|
||||
final String? badge;
|
||||
|
||||
/// Tap callback
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const QuickActionItem({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.badge,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
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(
|
||||
icon,
|
||||
size: 28,
|
||||
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: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/// Widget: Quick Action Section
|
||||
///
|
||||
/// Section container with title and grid of action items.
|
||||
/// Groups related actions together (e.g., Products & Cart, Loyalty, etc.)
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/features/home/presentation/widgets/quick_action_item.dart';
|
||||
|
||||
/// Quick Action Section Data Model
|
||||
class QuickAction {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String? badge;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const QuickAction({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.badge,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
|
||||
/// Quick Action Section Widget
|
||||
///
|
||||
/// Displays a titled card containing a grid of action buttons.
|
||||
/// Each section groups related functionality.
|
||||
class QuickActionSection extends StatelessWidget {
|
||||
/// Section title
|
||||
final String title;
|
||||
|
||||
/// List of actions in this section
|
||||
final List<QuickAction> actions;
|
||||
|
||||
const QuickActionSection({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.actions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section Title
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Action Grid
|
||||
_buildActionGrid(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionGrid() {
|
||||
// Determine grid layout based on number of items
|
||||
final int crossAxisCount = actions.length <= 2 ? 2 : 3;
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 1.0,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: actions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final action = actions[index];
|
||||
return QuickActionItem(
|
||||
icon: action.icon,
|
||||
label: action.label,
|
||||
badge: action.badge,
|
||||
onTap: action.onTap,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user