Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,256 @@
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';
import '../../services/coupon_service.dart';
import '../../theme/app_theme.dart';
import '../../widgets/custom_bottom_bar.dart';
import './widgets/category_filter_chips.dart';
import './widgets/coupon_card_widget.dart';
import './widgets/coupon_detail_modal.dart';
import './widgets/empty_state_widget.dart';
import './widgets/sort_dropdown.dart';
class CouponsScreen extends StatefulWidget {
const CouponsScreen({super.key});
@override
State<CouponsScreen> createState() => _CouponsScreenState();
}
class _CouponsScreenState extends State<CouponsScreen> {
final ScrollController _scrollController = ScrollController();
bool _isLoading = true;
bool _isRefreshing = false;
String _selectedCategory = 'Todos';
String _selectedSort = 'Más recientes';
List<CouponModel> _coupons = [];
@override
void initState() {
super.initState();
_loadCoupons();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
/// Load coupons with silent error handling - no user error messages
Future<void> _loadCoupons() async {
try {
setState(() {
_isLoading = true;
});
final coupons = await CouponService.getCoupons(
selectedCategory: _selectedCategory,
sort: _selectedSort,
);
setState(() {
_coupons = coupons;
_isLoading = false;
});
} catch (e) {
// Silent failure - show empty state but don't show error to user
setState(() {
_coupons = [];
_isLoading = false;
});
}
}
/// Refresh coupons with silent error handling
Future<void> _refreshCoupons() async {
setState(() {
_isRefreshing = true;
});
HapticFeedback.lightImpact();
try {
final coupons = await CouponService.getCoupons(
selectedCategory: _selectedCategory,
sort: _selectedSort,
);
setState(() {
_coupons = coupons;
});
} catch (e) {
// Silent failure - keep existing coupons, don't show error
} finally {
setState(() {
_isRefreshing = false;
});
}
}
/// Handle category change and auto-refresh data
void _onCategoryChanged(String category) {
if (_selectedCategory != category) {
setState(() {
_selectedCategory = category;
});
_loadCoupons(); // Automatically refresh when filters change
}
}
/// Handle sort change and auto-refresh data
void _onSortChanged(String sort) {
if (_selectedSort != sort) {
setState(() {
_selectedSort = sort;
});
_loadCoupons(); // Automatically refresh when sorting changes
}
}
void _showCouponDetail(CouponModel coupon) {
HapticFeedback.lightImpact();
showDialog(
context: context,
builder: (context) => CouponDetailModal(coupon: coupon),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
title: Text(
'Cupones',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
backgroundColor: theme.colorScheme.surface,
elevation: 0,
),
body: Column(
children: [
// Category Filter Chips (Spanish UI)
CategoryFilterChips(
selectedCategory: _selectedCategory,
onCategorySelected: _onCategoryChanged,
),
// Sort Dropdown (Spanish UI)
Container(
padding: EdgeInsets.symmetric(vertical: 1.h),
color: theme.colorScheme.surface,
child: SortDropdown(
selectedSort: _selectedSort,
onSortChanged: _onSortChanged,
),
),
// Content
Expanded(child: _buildContent()),
],
),
bottomNavigationBar: CustomBottomBar(
currentIndex: 2,
onTap: (index) {
switch (index) {
case 0:
Navigator.pushReplacementNamed(context, '/map-screen');
break;
case 1:
Navigator.pushReplacementNamed(context, '/schedules-screen');
break;
case 2:
// Already on coupons screen
break;
}
},
),
);
}
Widget _buildContent() {
final theme = Theme.of(context);
if (_isLoading) {
return Center(
child: CircularProgressIndicator(color: AppTheme.accentYellow),
);
}
// No error states shown to user - only empty states
if (_coupons.isEmpty) {
final isFiltered = _selectedCategory != 'Todos';
return EmptyStateWidget(
title:
isFiltered
? 'No hay cupones disponibles para esta categoría.'
: 'Aún no hay cupones registrados.',
subtitle:
isFiltered
? 'Intenta seleccionando otra categoría'
: 'Vuelve pronto para ver nuevas ofertas',
actionText: isFiltered ? 'Ver todos' : null,
onActionPressed: isFiltered ? () => _onCategoryChanged('Todos') : null,
);
}
return RefreshIndicator(
onRefresh: _refreshCoupons,
color: AppTheme.accentYellow,
backgroundColor: theme.colorScheme.surface,
child: GridView.builder(
controller: _scrollController,
padding: EdgeInsets.all(4.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _getGridCrossAxisCount(),
crossAxisSpacing: 4.w,
mainAxisSpacing: 4.w,
childAspectRatio: 0.75,
),
itemCount: _coupons.length + (_isRefreshing ? 2 : 0),
itemBuilder: (context, index) {
if (index >= _coupons.length) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.surface,
),
child: Center(
child: CircularProgressIndicator(
color: AppTheme.accentYellow,
strokeWidth: 2,
),
),
),
);
}
final coupon = _coupons[index];
return CouponCardWidget(
coupon: coupon,
onTap: () => _showCouponDetail(coupon),
);
},
),
);
}
int _getGridCrossAxisCount() {
if (MediaQuery.of(context).size.width > 768) {
return 3; // Tablet
}
return 2; // Phone
}
}

View File

@ -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,
),
),
),
);
},
),
);
}
}

View File

@ -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,
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -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),
),
),
),
],
),
],
),
),
),
],
),
),
);
}
}

View File

@ -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,
),
),
),
],
],
),
),
);
}
}

View File

@ -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),
],
),
);
}
}

View File

@ -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);
}
},
),
),
),
],
),
);
}
}