From 007b6562a6c5033617259dfb2d8024cd6501aa03 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:09:57 -0600 Subject: [PATCH 1/8] feat: reactivate and fully implement order book filter functionality - Implement comprehensive filtering logic for currencies, payment methods, premium/discount, and reputation - Implement real-time order filtering based on Nostr event tags - Convert rating filter to range-based selection with min/max values - Integrate dynamic data sources from exchange service and payment methods providers - Apply consistent Material Design styling across all filter components - Add proper async data handling with loading and error states - Ensure visual consistency with app theme and touch interactions --- .../home/providers/home_order_providers.dart | 55 +- lib/features/home/screens/home_screen.dart | 88 ++- lib/l10n/intl_en.arb | 13 + lib/l10n/intl_es.arb | 13 + lib/l10n/intl_it.arb | 13 + lib/shared/widgets/order_filter.dart | 652 ++++++++++++++++-- 6 files changed, 721 insertions(+), 113 deletions(-) diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index 12c551ef7..36c1ade8f 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -7,20 +7,67 @@ import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; final homeOrderTypeProvider = StateProvider((ref) => OrderType.sell); +// Filter state providers +final currencyFilterProvider = StateProvider>((ref) => []); +final paymentMethodFilterProvider = StateProvider>((ref) => []); +final ratingFilterProvider = StateProvider<({double min, double max})>((ref) => (min: 0.0, max: 5.0)); +final premiumRangeFilterProvider = StateProvider<({double min, double max})>((ref) => (min: -10.0, max: 10.0)); + final filteredOrdersProvider = Provider>((ref) { final allOrdersAsync = ref.watch(orderEventsProvider); final orderType = ref.watch(homeOrderTypeProvider); + final selectedCurrencies = ref.watch(currencyFilterProvider); + final selectedPaymentMethods = ref.watch(paymentMethodFilterProvider); + final ratingRange = ref.watch(ratingFilterProvider); + final premiumRange = ref.watch(premiumRangeFilterProvider); return allOrdersAsync.maybeWhen( data: (allOrders) { allOrders .sort((o1, o2) => o1.expirationDate.compareTo(o2.expirationDate)); - final filtered = allOrders.reversed + var filtered = allOrders.reversed .where((o) => o.orderType == orderType) - .where((o) => o.status == Status.pending) - .toList(); - return filtered; + .where((o) => o.status == Status.pending); + + // Apply currency filter + if (selectedCurrencies.isNotEmpty) { + filtered = filtered.where((o) => + o.currency != null && selectedCurrencies.contains(o.currency!) + ); + } + + // Apply payment method filter + if (selectedPaymentMethods.isNotEmpty) { + filtered = filtered.where((o) => + o.paymentMethods.isNotEmpty && + selectedPaymentMethods.any((method) => + o.paymentMethods.any((pm) => + pm.toLowerCase().contains(method.toLowerCase()) + ) + ) + ); + } + + // Apply rating filter + if (ratingRange.min > 0.0 || ratingRange.max < 5.0) { + filtered = filtered.where((o) => + o.rating != null && + o.rating!.totalRating >= ratingRange.min && + o.rating!.totalRating <= ratingRange.max + ); + } + + // Apply premium/discount filter + if (premiumRange.min > -10.0 || premiumRange.max < 10.0) { + filtered = filtered.where((o) { + if (o.premium == null || o.premium!.isEmpty) return false; + final premiumValue = double.tryParse(o.premium!) ?? 0.0; + return premiumValue >= premiumRange.min && premiumValue <= premiumRange.max; + }); + } + + return filtered.toList(); }, orElse: () => [], ); diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 29d2b2473..b86993677 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -8,6 +8,7 @@ import 'package:mostro_mobile/features/home/widgets/order_list_item.dart'; import 'package:mostro_mobile/shared/widgets/add_order_button.dart'; import 'package:mostro_mobile/shared/widgets/bottom_nav_bar.dart'; import 'package:mostro_mobile/shared/widgets/mostro_app_bar.dart'; +import 'package:mostro_mobile/shared/widgets/order_filter.dart'; import 'package:mostro_mobile/shared/widgets/custom_drawer_overlay.dart'; import 'package:mostro_mobile/generated/l10n.dart'; @@ -196,7 +197,6 @@ class HomeScreen extends ConsumerWidget { child: Align( alignment: Alignment.centerLeft, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppTheme.backgroundInput, borderRadius: BorderRadius.circular(30), @@ -209,40 +209,62 @@ class HomeScreen extends ConsumerWidget { ), ], ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HeroIcon( - HeroIcons.funnel, - style: HeroIconStyle.outline, - color: Colors.white70, - size: 18, - ), - const SizedBox(width: 8), - Text( - S.of(context)!.filter, - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - height: 16, - width: 1, - color: Colors.white.withValues(alpha: 0.2), - ), - Text( - S.of(context)!.offersCount(filteredOrders.length.toString()), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - fontWeight: FontWeight.normal, + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(30), + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const Dialog( + child: OrderFilter(), + ); + }, + ); + }, + borderRadius: BorderRadius.circular(30), + splashColor: AppTheme.activeColor.withValues(alpha: 0.3), + highlightColor: AppTheme.activeColor.withValues(alpha: 0.15), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HeroIcon( + HeroIcons.funnel, + style: HeroIconStyle.outline, + color: Colors.white70, + size: 18, + ), + const SizedBox(width: 8), + Text( + S.of(context)!.filter, + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + height: 16, + width: 1, + color: Colors.white.withValues(alpha: 0.2), + ), + Text( + S.of(context)!.offersCount(filteredOrders.length.toString()), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], ), ), - ], + ), ), ), ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index d6d1e2b18..0e5941d30 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -796,9 +796,22 @@ "save": "Save", + "apply": "Apply", "selectCurrency": "Select Currency", "noCurrencySelected": "No currency selected", + "@_comment_filter_section": "Filter section strings", + "fiatCurrencies": "Fiat currencies", + "paymentMethods": "Payment methods", + "rating": "Rating", + "reputation": "Reputation", + "premiumRange": "Premium/Discount", + "discount": "Discount", + "premium": "Premium", + "clear": "Clear", + "noneSelected": "None selected", + "typeToAdd": "Type to add...", + "@_comment_timeout_messages": "Timeout notification messages", "orderTimeoutTaker": "You didn't respond in time. The order will be republished", "orderTimeoutMaker": "Your counterpart didn't respond in time. The order will be republished", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 21edcc01c..492619b33 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -823,6 +823,19 @@ "add": "Agregar", "save": "Guardar", + "apply": "Aplicar", + + "@_comment_filter_section": "Strings de la sección de filtros", + "fiatCurrencies": "Monedas fiat", + "paymentMethods": "Métodos de pago", + "rating": "Calificación", + "reputation": "Reputación", + "premiumRange": "Prima/Descuento", + "discount": "Descuento", + "premium": "Prima", + "clear": "Limpiar", + "noneSelected": "Ninguna seleccionada", + "typeToAdd": "Escribe para agregar...", "selectCurrency": "Seleccionar Moneda", "noCurrencySelected": "Ninguna moneda seleccionada", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 70b780630..ab119431a 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -831,6 +831,19 @@ "add": "Aggiungi", "save": "Salva", + "apply": "Applica", + + "@_comment_filter_section": "Stringhe della sezione filtri", + "fiatCurrencies": "Valute fiat", + "paymentMethods": "Metodi di pagamento", + "rating": "Valutazione", + "reputation": "Reputazione", + "premiumRange": "Premio/Sconto", + "discount": "Sconto", + "premium": "Premio", + "clear": "Cancella", + "noneSelected": "Nessuna selezionata", + "typeToAdd": "Digita per aggiungere...", "selectCurrency": "Seleziona Valuta", "noCurrencySelected": "Nessuna valuta selezionata", diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index 90fc5faa5..3fde42c12 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/features/home/providers/home_order_providers.dart'; +import 'package:mostro_mobile/features/order/providers/payment_methods_provider.dart'; +import 'package:mostro_mobile/generated/l10n.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; /// A custom multi-select field based on Autocomplete. /// It lets the user type to filter options and add selections which are shown as Chips. @@ -44,7 +49,12 @@ class MultiSelectAutocompleteState extends State { children: [ Text( widget.label, - style: const TextStyle(color: AppTheme.mostroGreen), + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), ), const SizedBox(height: 8), Autocomplete( @@ -58,6 +68,46 @@ class MultiSelectAutocompleteState extends State { .contains(textEditingValue.text.toLowerCase()) && !widget.selectedValues.contains(option)); }, + optionsViewBuilder: (context, onSelected, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + color: AppTheme.backgroundCard, + elevation: 8, + borderRadius: BorderRadius.circular(8), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300), + child: ListView.builder( + padding: const EdgeInsets.all(4), + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (context, index) { + final option = options.elementAt(index); + return InkWell( + onTap: () => onSelected(option), + borderRadius: BorderRadius.circular(6), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + margin: const EdgeInsets.symmetric(vertical: 1), + child: Text( + option, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ); + }, + ), + ), + ), + ); + }, onSelected: (String selection) { final updated = List.from(widget.selectedValues) ..add(selection); @@ -70,9 +120,43 @@ class MultiSelectAutocompleteState extends State { return TextFormField( controller: textEditingController, focusNode: focusNode, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + ), decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: 'Type to add...', + filled: true, + fillColor: AppTheme.backgroundInput, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: AppTheme.textSecondary, + width: 1.5, + ), + ), + hintText: S.of(context)!.typeToAdd, + hintStyle: TextStyle( + color: AppTheme.textInactive, + fontSize: 14, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, + ), ), ); }, @@ -82,20 +166,69 @@ class MultiSelectAutocompleteState extends State { spacing: 8, children: widget.selectedValues.isEmpty ? [ - const Text( - 'None selected', - style: TextStyle(color: AppTheme.cream1), + Text( + S.of(context)!.noneSelected, + style: TextStyle( + color: AppTheme.textInactive, + fontSize: 13, + fontStyle: FontStyle.italic, + ), ) ] : widget.selectedValues - .map((value) => Chip( - label: Text(value), - onDeleted: () { - final updated = - List.from(widget.selectedValues) - ..remove(value); - widget.onChanged(updated); - }, + .map((value) => Container( + margin: const EdgeInsets.only(right: 6, bottom: 4), + child: Material( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.textSecondary.withValues(alpha: 0.6), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 6), + GestureDetector( + onTap: () { + final updated = List.from(widget.selectedValues) + ..remove(value); + widget.onChanged(updated); + }, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: AppTheme.textSecondary.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + size: 14, + color: AppTheme.textSecondary, + ), + ), + ), + ], + ), + ), + ), )) .toList(), ), @@ -106,41 +239,77 @@ class MultiSelectAutocompleteState extends State { } /// The updated OrderFilter widget which uses the MultiSelectAutocomplete widgets and a slider. -class OrderFilter extends StatefulWidget { +class OrderFilter extends ConsumerStatefulWidget { const OrderFilter({super.key}); @override - OrderFilterState createState() => OrderFilterState(); + ConsumerState createState() => OrderFilterState(); } -class OrderFilterState extends State { +class OrderFilterState extends ConsumerState { List selectedFiatCurrencies = []; List selectedPaymentMethods = []; - double rating = 0.0; + double ratingMin = 0.0; + double ratingMax = 5.0; + double premiumMin = -10.0; + double premiumMax = 10.0; // Options for the multi-select fields. - final List fiatOptions = ['USD', 'EUR', 'VES']; - final List paymentMethodsOptions = [ - 'face to face', - 'bank transfer', - 'lightning' - ]; + + List _getAllPaymentMethods(Map paymentMethodsData) { + final Set allMethods = {}; + + // Add all payment methods from all currencies + for (final methods in paymentMethodsData.values) { + if (methods is List) { + allMethods.addAll(methods.cast()); + } + } + + // Remove "Other" since it's for custom input, not filtering + allMethods.remove('Other'); + + final sortedMethods = allMethods.toList()..sort(); + return sortedMethods; + } + + @override + void initState() { + super.initState(); + // Load current filter values from providers + WidgetsBinding.instance.addPostFrameCallback((_) { + final currencies = ref.read(currencyFilterProvider); + final paymentMethods = ref.read(paymentMethodFilterProvider); + final currentRatingRange = ref.read(ratingFilterProvider); + final currentPremiumRange = ref.read(premiumRangeFilterProvider); + + setState(() { + selectedFiatCurrencies = List.from(currencies); + selectedPaymentMethods = List.from(paymentMethods); + ratingMin = currentRatingRange is double ? 0.0 : currentRatingRange.min; + ratingMax = currentRatingRange is double ? currentRatingRange as double : currentRatingRange.max; + premiumMin = currentPremiumRange.min; + premiumMax = currentPremiumRange.max; + }); + }); + } @override Widget build(BuildContext context) { + final currenciesAsync = ref.watch(currencyCodesProvider); + final paymentMethodsAsync = ref.watch(paymentMethodsDataProvider); + return Container( - width: 300, - padding: const EdgeInsets.all(16), + width: 320, + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: AppTheme.cream1, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: .1), - blurRadius: 5, - offset: const Offset(0, 3), - ), - ], + color: AppTheme.backgroundCard, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withValues(alpha: 0.1), + width: 1, + ), + boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -151,73 +320,404 @@ class OrderFilterState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( - children: const [ - HeroIcon(HeroIcons.funnel, - style: HeroIconStyle.outline, color: AppTheme.dark2), - SizedBox(width: 8), + children: [ + const HeroIcon(HeroIcons.funnel, + style: HeroIconStyle.outline, color: AppTheme.mostroGreen), + const SizedBox(width: 8), Text( - 'FILTER', - style: TextStyle( - color: AppTheme.dark2, + S.of(context)!.filter.toUpperCase(), + style: const TextStyle( + color: AppTheme.textPrimary, fontSize: 18, fontWeight: FontWeight.bold, + letterSpacing: 0.5, ), ), ], ), IconButton( - icon: const Icon(Icons.close, color: AppTheme.dark2, size: 20), + icon: const Icon(Icons.close, color: AppTheme.textSecondary, size: 22), onPressed: () { Navigator.of(context).pop(); }, + style: IconButton.styleFrom( + backgroundColor: Colors.white.withValues(alpha: 0.1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(6), + ), ), ], ), const SizedBox(height: 20), // Fiat currencies using Autocomplete multi-select. - MultiSelectAutocomplete( - label: 'Fiat currencies', - options: fiatOptions, - selectedValues: selectedFiatCurrencies, - onChanged: (values) { - setState(() { - selectedFiatCurrencies = values; - }); - }, + currenciesAsync.when( + data: (currencies) => MultiSelectAutocomplete( + label: S.of(context)!.fiatCurrencies, + options: currencies.keys.toList()..sort(), + selectedValues: selectedFiatCurrencies, + onChanged: (values) { + setState(() { + selectedFiatCurrencies = values; + }); + }, + ), + loading: () => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.fiatCurrencies, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + height: 48, + decoration: BoxDecoration( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + ), + child: const Center( + child: Text( + 'Loading currencies...', + style: TextStyle( + color: AppTheme.textInactive, + fontSize: 14, + ), + ), + ), + ), + const SizedBox(height: 8), + ], + ), + error: (error, stack) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.fiatCurrencies, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 8), + MultiSelectAutocomplete( + label: S.of(context)!.fiatCurrencies, + options: ['USD', 'EUR', 'VES'], // Fallback options + selectedValues: selectedFiatCurrencies, + onChanged: (values) { + setState(() { + selectedFiatCurrencies = values; + }); + }, + ), + ], + ), ), const SizedBox(height: 12), // Payment methods using Autocomplete multi-select. - MultiSelectAutocomplete( - label: 'Payment methods', - options: paymentMethodsOptions, - selectedValues: selectedPaymentMethods, - onChanged: (values) { - setState(() { - selectedPaymentMethods = values; - }); - }, + paymentMethodsAsync.when( + data: (paymentMethodsData) => MultiSelectAutocomplete( + label: S.of(context)!.paymentMethods, + options: _getAllPaymentMethods(paymentMethodsData), + selectedValues: selectedPaymentMethods, + onChanged: (values) { + setState(() { + selectedPaymentMethods = values; + }); + }, + ), + loading: () => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.paymentMethods, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + height: 48, + decoration: BoxDecoration( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + ), + child: const Center( + child: Text( + 'Loading payment methods...', + style: TextStyle( + color: AppTheme.textInactive, + fontSize: 14, + ), + ), + ), + ), + const SizedBox(height: 8), + ], + ), + error: (error, stack) => MultiSelectAutocomplete( + label: S.of(context)!.paymentMethods, + options: ['Bank Transfer', 'Cash in person', 'PayPal', 'Zelle'], // Fallback options + selectedValues: selectedPaymentMethods, + onChanged: (values) { + setState(() { + selectedPaymentMethods = values; + }); + }, + ), ), const SizedBox(height: 12), - // Rating slider between 0 and 5. + // Premium/Discount range filter Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Rating: ${rating.toStringAsFixed(1)}", - style: const TextStyle(color: AppTheme.mostroGreen), + S.of(context)!.premiumRange, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + "${S.of(context)!.discount}: ${premiumMin.toInt()}%", + style: const TextStyle( + color: AppTheme.sellColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + Text( + "${S.of(context)!.premium}: ${premiumMax.toInt()}%", + style: const TextStyle( + color: AppTheme.buyColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppTheme.textSecondary, + inactiveTrackColor: AppTheme.backgroundInput, + thumbColor: AppTheme.textSecondary, + overlayColor: AppTheme.textSecondary.withValues(alpha: 0.2), + valueIndicatorColor: AppTheme.textSecondary, + valueIndicatorTextStyle: const TextStyle( + color: AppTheme.backgroundDark, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + trackHeight: 4, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + ), + child: RangeSlider( + values: RangeValues(premiumMin, premiumMax), + min: -10.0, + max: 10.0, + divisions: 20, + labels: RangeLabels( + "${premiumMin.toInt()}%", + "${premiumMax.toInt()}%" + ), + onChanged: (values) { + setState(() { + premiumMin = values.start; + premiumMax = values.end; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 12), + // Rating range slider between 0 and 5. + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.reputation, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + "Min: ${ratingMin.toInt()}", + style: const TextStyle( + color: AppTheme.sellColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + Text( + "Max: ${ratingMax.toInt()}", + style: const TextStyle( + color: AppTheme.buyColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppTheme.textSecondary, + inactiveTrackColor: AppTheme.backgroundInput, + thumbColor: AppTheme.textSecondary, + overlayColor: AppTheme.textSecondary.withValues(alpha: 0.2), + valueIndicatorColor: AppTheme.textSecondary, + valueIndicatorTextStyle: const TextStyle( + color: AppTheme.backgroundDark, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + trackHeight: 4, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + ), + child: RangeSlider( + values: RangeValues(ratingMin, ratingMax), + min: 0.0, + max: 5.0, + divisions: 5, + labels: RangeLabels( + ratingMin.toInt().toString(), + ratingMax.toInt().toString() + ), + onChanged: (values) { + setState(() { + ratingMin = values.start; + ratingMax = values.end; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + // Apply and Clear buttons + Row( + children: [ + Flexible( + flex: 1, + child: Container( + width: double.infinity, + height: 50, + child: OutlinedButton( + onPressed: () { + // Clear all filters + setState(() { + selectedFiatCurrencies.clear(); + selectedPaymentMethods.clear(); + ratingMin = 0.0; + ratingMax = 5.0; + premiumMin = -10.0; + premiumMax = 10.0; + }); + + ref.read(currencyFilterProvider.notifier).state = []; + ref.read(paymentMethodFilterProvider.notifier).state = []; + ref.read(ratingFilterProvider.notifier).state = (min: 0.0, max: 5.0); + ref.read(premiumRangeFilterProvider.notifier).state = (min: -10.0, max: 10.0); + + Navigator.of(context).pop(); + }, + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.textSecondary, + side: BorderSide( + color: AppTheme.textSecondary.withValues(alpha: 0.3), + width: 1, + ), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + S.of(context)!.clear.toUpperCase(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Flexible( + flex: 1, + child: Container( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () { + // Apply filters to providers + ref.read(currencyFilterProvider.notifier).state = selectedFiatCurrencies; + ref.read(paymentMethodFilterProvider.notifier).state = selectedPaymentMethods; + ref.read(ratingFilterProvider.notifier).state = (min: ratingMin, max: ratingMax); + ref.read(premiumRangeFilterProvider.notifier).state = (min: premiumMin, max: premiumMax); + + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + foregroundColor: AppTheme.backgroundDark, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + shadowColor: Colors.transparent, + ), + child: Text( + S.of(context)!.apply.toUpperCase(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + ), + ), + ), + ), ), - Slider( - value: rating, - min: 0, - max: 5, - divisions: 5, - label: rating.toStringAsFixed(1), - onChanged: (val) { - setState(() { - rating = val; - }); - }, - ) ], ), ], From f80a5a3cd61295da0527a5aa09f3ead41aaa3f51 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:29:40 -0600 Subject: [PATCH 2/8] fix: replace Container with SizedBox for whitespace in order filter --- lib/shared/widgets/order_filter.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index 3fde42c12..f32f3985b 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -637,7 +637,7 @@ class OrderFilterState extends ConsumerState { children: [ Flexible( flex: 1, - child: Container( + child: SizedBox( width: double.infinity, height: 50, child: OutlinedButton( @@ -684,7 +684,7 @@ class OrderFilterState extends ConsumerState { const SizedBox(width: 12), Flexible( flex: 1, - child: Container( + child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( From b3e603c1a03e83e1ae93a7b38f8373fc7afc13a3 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:45:13 -0600 Subject: [PATCH 3/8] Fix duplicate ARB keys --- lib/l10n/intl_en.arb | 4 ---- lib/l10n/intl_es.arb | 4 ---- lib/l10n/intl_it.arb | 4 ---- 3 files changed, 12 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0e5941d30..88bb8364b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -801,16 +801,12 @@ "noCurrencySelected": "No currency selected", "@_comment_filter_section": "Filter section strings", - "fiatCurrencies": "Fiat currencies", - "paymentMethods": "Payment methods", "rating": "Rating", "reputation": "Reputation", "premiumRange": "Premium/Discount", "discount": "Discount", "premium": "Premium", "clear": "Clear", - "noneSelected": "None selected", - "typeToAdd": "Type to add...", "@_comment_timeout_messages": "Timeout notification messages", "orderTimeoutTaker": "You didn't respond in time. The order will be republished", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 492619b33..11f5aee4c 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -826,16 +826,12 @@ "apply": "Aplicar", "@_comment_filter_section": "Strings de la sección de filtros", - "fiatCurrencies": "Monedas fiat", - "paymentMethods": "Métodos de pago", "rating": "Calificación", "reputation": "Reputación", "premiumRange": "Prima/Descuento", "discount": "Descuento", "premium": "Prima", "clear": "Limpiar", - "noneSelected": "Ninguna seleccionada", - "typeToAdd": "Escribe para agregar...", "selectCurrency": "Seleccionar Moneda", "noCurrencySelected": "Ninguna moneda seleccionada", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index ab119431a..1b796252f 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -834,16 +834,12 @@ "apply": "Applica", "@_comment_filter_section": "Stringhe della sezione filtri", - "fiatCurrencies": "Valute fiat", - "paymentMethods": "Metodi di pagamento", "rating": "Valutazione", "reputation": "Reputazione", "premiumRange": "Premio/Sconto", "discount": "Sconto", "premium": "Premio", "clear": "Cancella", - "noneSelected": "Nessuna selezionata", - "typeToAdd": "Digita per aggiungere...", "selectCurrency": "Seleziona Valuta", "noCurrencySelected": "Nessuna valuta selezionata", From f942b3aa894e1ce686b46f50d2049e9e7d7b33c7 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:00:31 -0600 Subject: [PATCH 4/8] refactor: optimize payment method filter performance and null-safety --- .../home/providers/home_order_providers.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index 36c1ade8f..880cf785f 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -39,14 +39,18 @@ final filteredOrdersProvider = Provider>((ref) { // Apply payment method filter if (selectedPaymentMethods.isNotEmpty) { - filtered = filtered.where((o) => - o.paymentMethods.isNotEmpty && - selectedPaymentMethods.any((method) => - o.paymentMethods.any((pm) => - pm.toLowerCase().contains(method.toLowerCase()) - ) - ) - ); + final methodsLower = selectedPaymentMethods + .where((m) => m.trim().isNotEmpty) + .map((m) => m.toLowerCase()) + .toSet(); + filtered = filtered.where((o) { + final pms = o.paymentMethods; + if (pms == null || pms.isEmpty) return false; + return pms.any((pm) { + final pmLower = pm.toLowerCase(); + return methodsLower.any(pmLower.contains); + }); + }); } // Apply rating filter From c3ff1274c48b76b364715ef6b600fee060a2ec02 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:10:47 -0600 Subject: [PATCH 5/8] fix: remove unnecessary null comparison in payment method filter --- lib/features/home/providers/home_order_providers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index 880cf785f..ee9e16b78 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -45,7 +45,7 @@ final filteredOrdersProvider = Provider>((ref) { .toSet(); filtered = filtered.where((o) { final pms = o.paymentMethods; - if (pms == null || pms.isEmpty) return false; + if (pms.isEmpty) return false; return pms.any((pm) { final pmLower = pm.toLowerCase(); return methodsLower.any(pmLower.contains); From 08386db60eec4a0d7c07e0f712abfcba85168894 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 19:05:19 -0600 Subject: [PATCH 6/8] fix: add mounted check and use record destructuring in filter initState --- lib/shared/widgets/order_filter.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index f32f3985b..2e78ef3a9 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -278,6 +278,8 @@ class OrderFilterState extends ConsumerState { super.initState(); // Load current filter values from providers WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final currencies = ref.read(currencyFilterProvider); final paymentMethods = ref.read(paymentMethodFilterProvider); final currentRatingRange = ref.read(ratingFilterProvider); @@ -286,8 +288,9 @@ class OrderFilterState extends ConsumerState { setState(() { selectedFiatCurrencies = List.from(currencies); selectedPaymentMethods = List.from(paymentMethods); - ratingMin = currentRatingRange is double ? 0.0 : currentRatingRange.min; - ratingMax = currentRatingRange is double ? currentRatingRange as double : currentRatingRange.max; + final (min: rMin, max: rMax) = currentRatingRange; + ratingMin = rMin; + ratingMax = rMax; premiumMin = currentPremiumRange.min; premiumMax = currentPremiumRange.max; }); From 845e632bc975313931830136189e8a3b18a8b285 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 19:52:49 -0600 Subject: [PATCH 7/8] feat: localize loading states and min/max labels in order filter --- lib/l10n/intl_en.arb | 4 ++++ lib/l10n/intl_es.arb | 4 ++++ lib/l10n/intl_it.arb | 4 ++++ lib/shared/widgets/order_filter.dart | 16 ++++++++-------- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 88bb8364b..045ae6d1d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -807,6 +807,10 @@ "discount": "Discount", "premium": "Premium", "clear": "Clear", + "loadingCurrencies": "Loading currencies...", + "loadingPaymentMethods": "Loading payment methods...", + "min": "Min", + "max": "Max", "@_comment_timeout_messages": "Timeout notification messages", "orderTimeoutTaker": "You didn't respond in time. The order will be republished", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 11f5aee4c..f89f2ff0d 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -832,6 +832,10 @@ "discount": "Descuento", "premium": "Prima", "clear": "Limpiar", + "loadingCurrencies": "Cargando monedas...", + "loadingPaymentMethods": "Cargando métodos de pago...", + "min": "Mín", + "max": "Máx", "selectCurrency": "Seleccionar Moneda", "noCurrencySelected": "Ninguna moneda seleccionada", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 1b796252f..7af8c8896 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -840,6 +840,10 @@ "discount": "Sconto", "premium": "Premio", "clear": "Cancella", + "loadingCurrencies": "Caricamento valute...", + "loadingPaymentMethods": "Caricamento metodi di pagamento...", + "min": "Min", + "max": "Max", "selectCurrency": "Seleziona Valuta", "noCurrencySelected": "Nessuna valuta selezionata", diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index 2e78ef3a9..4a9d12827 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -390,10 +390,10 @@ class OrderFilterState extends ConsumerState { width: 1, ), ), - child: const Center( + child: Center( child: Text( - 'Loading currencies...', - style: TextStyle( + S.of(context)!.loadingCurrencies, + style: const TextStyle( color: AppTheme.textInactive, fontSize: 14, ), @@ -466,10 +466,10 @@ class OrderFilterState extends ConsumerState { width: 1, ), ), - child: const Center( + child: Center( child: Text( - 'Loading payment methods...', - style: TextStyle( + S.of(context)!.loadingPaymentMethods, + style: const TextStyle( color: AppTheme.textInactive, fontSize: 14, ), @@ -580,7 +580,7 @@ class OrderFilterState extends ConsumerState { Row( children: [ Text( - "Min: ${ratingMin.toInt()}", + "${S.of(context)!.min}: ${ratingMin.toInt()}", style: const TextStyle( color: AppTheme.sellColor, fontSize: 12, @@ -589,7 +589,7 @@ class OrderFilterState extends ConsumerState { ), const Spacer(), Text( - "Max: ${ratingMax.toInt()}", + "${S.of(context)!.max}: ${ratingMax.toInt()}", style: const TextStyle( color: AppTheme.buyColor, fontSize: 12, From 1b9a2dfb247ad7e956324a547e3b868ab4a427cd Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:10:00 -0600 Subject: [PATCH 8/8] fix: prevent filter dialog overflow with scrollable content and fixed action buttons and remove duplicate ARB keys for loading messages --- lib/l10n/intl_en.arb | 2 -- lib/l10n/intl_es.arb | 2 -- lib/l10n/intl_it.arb | 2 -- lib/shared/widgets/order_filter.dart | 18 ++++++++++++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 045ae6d1d..c43f77c24 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -807,8 +807,6 @@ "discount": "Discount", "premium": "Premium", "clear": "Clear", - "loadingCurrencies": "Loading currencies...", - "loadingPaymentMethods": "Loading payment methods...", "min": "Min", "max": "Max", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index f89f2ff0d..f42d88e0f 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -832,8 +832,6 @@ "discount": "Descuento", "premium": "Prima", "clear": "Limpiar", - "loadingCurrencies": "Cargando monedas...", - "loadingPaymentMethods": "Cargando métodos de pago...", "min": "Mín", "max": "Máx", "selectCurrency": "Seleccionar Moneda", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 7af8c8896..c5a5b5482 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -840,8 +840,6 @@ "discount": "Sconto", "premium": "Premio", "clear": "Cancella", - "loadingCurrencies": "Caricamento valute...", - "loadingPaymentMethods": "Caricamento metodi di pagamento...", "min": "Min", "max": "Max", "selectCurrency": "Seleziona Valuta", diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index 4a9d12827..600259635 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -304,6 +304,7 @@ class OrderFilterState extends ConsumerState { return Container( width: 320, + height: MediaQuery.of(context).size.height * 0.8, // 80% of screen height padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppTheme.backgroundCard, @@ -316,7 +317,6 @@ class OrderFilterState extends ConsumerState { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ // Header with title and close button. Row( @@ -354,8 +354,14 @@ class OrderFilterState extends ConsumerState { ], ), const SizedBox(height: 20), - // Fiat currencies using Autocomplete multi-select. - currenciesAsync.when( + // Scrollable content area + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Fiat currencies using Autocomplete multi-select. + currenciesAsync.when( data: (currencies) => MultiSelectAutocomplete( label: S.of(context)!.fiatCurrencies, options: currencies.keys.toList()..sort(), @@ -633,9 +639,13 @@ class OrderFilterState extends ConsumerState { ), ), ], + ), + ], + ), + ), ), const SizedBox(height: 20), - // Apply and Clear buttons + // Apply and Clear buttons (always visible at bottom) Row( children: [ Flexible(