Initial commit: SIBU 2.0 MISSION
This commit is contained in:
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
import '../../../core/app_export.dart';
|
||||
import '../../../services/coupon_service.dart';
|
||||
import '../../../theme/app_theme.dart';
|
||||
|
||||
class CategoryFilterChips extends StatelessWidget {
|
||||
final String selectedCategory;
|
||||
final ValueChanged<String> onCategorySelected;
|
||||
|
||||
const CategoryFilterChips({
|
||||
super.key,
|
||||
required this.selectedCategory,
|
||||
required this.onCategorySelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final categories = CouponService.getCategoryOptions();
|
||||
|
||||
return Container(
|
||||
height: 6.h,
|
||||
padding: EdgeInsets.symmetric(vertical: 1.h),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
itemCount: categories.length,
|
||||
separatorBuilder: (context, index) => SizedBox(width: 2.w),
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final isSelected = selectedCategory == category;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onCategorySelected(category);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4.w,
|
||||
vertical: 1.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppTheme.accentYellow
|
||||
: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppTheme.accentYellow
|
||||
: theme.colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
category,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: isSelected
|
||||
? AppTheme.primaryBlack
|
||||
: theme.colorScheme.onSurface,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
import '../../../core/app_export.dart';
|
||||
import '../../../models/coupon_model.dart';
|
||||
|
||||
class CouponCardWidget extends StatelessWidget {
|
||||
final CouponModel coupon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const CouponCardWidget({
|
||||
super.key,
|
||||
required this.coupon,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image Section (16:9 aspect ratio)
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: coupon.imageUrl != null
|
||||
? CustomImageWidget(
|
||||
imageUrl: coupon.imageUrl!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
semanticLabel: 'Imagen de ${coupon.businessName}',
|
||||
)
|
||||
: Container(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomIconWidget(
|
||||
iconName: 'image',
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
Text(
|
||||
'Sin imagen',
|
||||
style:
|
||||
theme.textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content Section
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(3.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Business Name (bold)
|
||||
Text(
|
||||
coupon.businessName,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 0.5.h),
|
||||
// Title (subtitle style)
|
||||
Text(
|
||||
coupon.title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 0.5.h),
|
||||
// Description (2 lines max, small text)
|
||||
Expanded(
|
||||
child: Text(
|
||||
coupon.description,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
// Valid until text
|
||||
Text(
|
||||
coupon.validUntil != null
|
||||
? 'Válido hasta: ${coupon.validUntilFormatted}'
|
||||
: 'Sin fecha de vencimiento',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: coupon.isExpired
|
||||
? AppTheme.errorRed
|
||||
: coupon.isExpiringSoon
|
||||
? Colors.orange
|
||||
: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
import '../../../core/app_export.dart';
|
||||
import '../../../models/coupon_model.dart';
|
||||
|
||||
class CouponDetailModal extends StatelessWidget {
|
||||
final CouponModel coupon;
|
||||
final VoidCallback? onUseCoupon;
|
||||
|
||||
const CouponDetailModal({
|
||||
super.key,
|
||||
required this.coupon,
|
||||
this.onUseCoupon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: EdgeInsets.all(4.w),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 85.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Close Button
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2.w),
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: CustomIconWidget(
|
||||
iconName: 'close',
|
||||
color: theme.colorScheme.onSurface,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(6.w, 0, 6.w, 6.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Full Image
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 25.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: coupon.imageUrl != null
|
||||
? CustomImageWidget(
|
||||
imageUrl: coupon.imageUrl!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
semanticLabel:
|
||||
'Imagen de ${coupon.businessName}',
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomIconWidget(
|
||||
iconName: 'image',
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
size: 32,
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
Text(
|
||||
'Sin imagen',
|
||||
style:
|
||||
theme.textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
// Business Name
|
||||
Text(
|
||||
coupon.businessName,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
// Title
|
||||
Text(
|
||||
coupon.title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: AppTheme.accentYellow,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
// Description
|
||||
Text(
|
||||
coupon.description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
// Valid Until
|
||||
Container(
|
||||
padding: EdgeInsets.all(3.w),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CustomIconWidget(
|
||||
iconName: 'access_time',
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
Text(
|
||||
coupon.validUntil != null
|
||||
? 'Válido hasta: ${coupon.validUntilFormatted}'
|
||||
: 'Sin fecha de vencimiento',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: coupon.isExpired
|
||||
? AppTheme.errorRed
|
||||
: coupon.isExpiringSoon
|
||||
? Colors.orange
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
// Action Buttons Row
|
||||
Row(
|
||||
children: [
|
||||
// Call Button (placeholder)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// TODO: Implement call functionality
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Función de llamada próximamente'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: CustomIconWidget(
|
||||
iconName: 'call',
|
||||
color: theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text('Llamar'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
side:
|
||||
BorderSide(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
// WhatsApp Button (placeholder)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// TODO: Implement WhatsApp functionality
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Función de WhatsApp próximamente'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: CustomIconWidget(
|
||||
iconName: 'chat',
|
||||
color: Colors.green,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text('WhatsApp'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.green,
|
||||
side:
|
||||
BorderSide(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
// Location Button (placeholder)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// TODO: Implement location functionality
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Función de ubicación próximamente'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: CustomIconWidget(
|
||||
iconName: 'location_on',
|
||||
color: AppTheme.errorRed,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text('Ubicación'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.errorRed,
|
||||
side:
|
||||
BorderSide(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
import '../../../core/app_export.dart';
|
||||
|
||||
class EmptyStateWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String? actionText;
|
||||
final VoidCallback? onActionPressed;
|
||||
|
||||
const EmptyStateWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.actionText,
|
||||
this.onActionPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.w),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Empty state icon
|
||||
Container(
|
||||
padding: EdgeInsets.all(6.w),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: CustomIconWidget(
|
||||
iconName: 'local_offer',
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
// Subtitle
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (actionText != null && onActionPressed != null) ...[
|
||||
SizedBox(height: 4.h),
|
||||
// Action button
|
||||
ElevatedButton(
|
||||
onPressed: onActionPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentYellow,
|
||||
foregroundColor: AppTheme.primaryBlack,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8.w,
|
||||
vertical: 1.5.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
actionText!,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,273 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
import '../../../core/app_export.dart';
|
||||
import '../../../theme/app_theme.dart';
|
||||
|
||||
class FilterBottomSheet extends StatefulWidget {
|
||||
final Map<String, dynamic> currentFilters;
|
||||
final Function(Map<String, dynamic>) onFiltersChanged;
|
||||
|
||||
const FilterBottomSheet({
|
||||
super.key,
|
||||
required this.currentFilters,
|
||||
required this.onFiltersChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FilterBottomSheet> createState() => _FilterBottomSheetState();
|
||||
}
|
||||
|
||||
class _FilterBottomSheetState extends State<FilterBottomSheet> {
|
||||
late Map<String, dynamic> _filters;
|
||||
|
||||
final List<String> _categories = [
|
||||
'Todos',
|
||||
'Restaurantes',
|
||||
'Tiendas',
|
||||
'Servicios',
|
||||
'Entretenimiento',
|
||||
'Salud',
|
||||
'Belleza',
|
||||
];
|
||||
|
||||
final List<String> _sortOptions = [
|
||||
'Más recientes',
|
||||
'Por vencer',
|
||||
'Mayor descuento',
|
||||
'Distancia',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_filters = Map<String, dynamic>.from(widget.currentFilters);
|
||||
}
|
||||
|
||||
void _applyFilters() {
|
||||
HapticFeedback.lightImpact();
|
||||
widget.onFiltersChanged(_filters);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _resetFilters() {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
_filters = {
|
||||
'category': 'Todos',
|
||||
'sortBy': 'Más recientes',
|
||||
'showUsed': false,
|
||||
'showExpired': false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle bar
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 2.h),
|
||||
width: 12.w,
|
||||
height: 0.5.h,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.outline,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// Header
|
||||
Padding(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Filtros',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _resetFilters,
|
||||
child: Text(
|
||||
'Limpiar',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: theme.colorScheme.outline),
|
||||
// Content
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Category Filter
|
||||
Text(
|
||||
'Categoría',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Wrap(
|
||||
spacing: 2.w,
|
||||
runSpacing: 1.h,
|
||||
children: _categories.map((category) {
|
||||
final isSelected = _filters['category'] == category;
|
||||
return FilterChip(
|
||||
label: Text(category),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
_filters['category'] = category;
|
||||
});
|
||||
},
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
selectedColor:
|
||||
AppTheme.accentYellow.withValues(alpha: 0.2),
|
||||
checkmarkColor: AppTheme.primaryBlack,
|
||||
labelStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: isSelected
|
||||
? AppTheme.primaryBlack
|
||||
: theme.colorScheme.onSurface,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? AppTheme.accentYellow
|
||||
: theme.colorScheme.outline,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
SizedBox(height: 3.h),
|
||||
// Sort Options
|
||||
Text(
|
||||
'Ordenar por',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Column(
|
||||
children: _sortOptions.map((option) {
|
||||
final isSelected = _filters['sortBy'] == option;
|
||||
return RadioListTile<String>(
|
||||
title: Text(
|
||||
option,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
value: option,
|
||||
groupValue: _filters['sortBy'],
|
||||
onChanged: (value) {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
_filters['sortBy'] = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.primaryBlack,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
// Additional Options
|
||||
Text(
|
||||
'Mostrar',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
'Cupones usados',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
value: _filters['showUsed'] ?? false,
|
||||
onChanged: (value) {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
_filters['showUsed'] = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.accentYellow,
|
||||
activeTrackColor: AppTheme.primaryBlack,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
'Cupones vencidos',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
value: _filters['showExpired'] ?? false,
|
||||
onChanged: (value) {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
_filters['showExpired'] = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.accentYellow,
|
||||
activeTrackColor: AppTheme.primaryBlack,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Apply Button
|
||||
Padding(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 6.h,
|
||||
child: ElevatedButton(
|
||||
onPressed: _applyFilters,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentYellow,
|
||||
foregroundColor: AppTheme.primaryBlack,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Aplicar Filtros',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
import '../../../core/app_export.dart';
|
||||
import '../../../services/coupon_service.dart';
|
||||
import '../../../widgets/custom_icon_widget.dart';
|
||||
|
||||
class SortDropdown extends StatelessWidget {
|
||||
final String selectedSort;
|
||||
final ValueChanged<String> onSortChanged;
|
||||
|
||||
const SortDropdown({
|
||||
super.key,
|
||||
required this.selectedSort,
|
||||
required this.onSortChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final sortOptions = CouponService.getSortOptions();
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Ordenar por:',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 3.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedSort,
|
||||
isDense: true,
|
||||
icon: CustomIconWidget(
|
||||
iconName: 'keyboard_arrow_down',
|
||||
color: theme.colorScheme.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
items: sortOptions.map((String option) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: option,
|
||||
child: Text(
|
||||
option,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
if (newValue != null) {
|
||||
HapticFeedback.lightImpact();
|
||||
onSortChanged(newValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user