diff --git a/lib/core/app_theme.dart b/lib/core/app_theme.dart index 6c48b055..6871d85a 100644 --- a/lib/core/app_theme.dart +++ b/lib/core/app_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; class AppTheme { - // Color Definitions + // Original colors static const Color grey = Color(0xFFCCCCCC); static const Color mostroGreen = Color(0xFF8CC541); static const Color dark1 = Color(0xFF1D212C); @@ -14,7 +14,34 @@ class AppTheme { static const Color red2 = Color(0xFFE45A5A); static const Color green2 = Color(0xFF739C3D); - // Padding and Margin Constants + // New colors + + // Colors for backgrounds + static const Color backgroundDark = + Color(0xFF171A23); // Main dark background + static const Color backgroundCard = Color(0xFF1E2230); + static const Color backgroundInput = Color(0xFF252A3A); + static const Color backgroundInactive = Color(0xFF2A3042); + static const Color backgroundNavBar = Color(0xFF1A1F2C); + + // Colors for text + static const Color textPrimary = Colors.white; + static const Color textSecondary = Color(0xFFCCCCCC); + static const Color textInactive = Color(0xFF8A8D98); + static const Color textSubtle = Colors.white60; + + // Colors for actions + static const Color buyColor = Color(0xFF8CC63F); + static const Color sellColor = Color(0xFFEA384C); + static const Color activeColor = Color(0xFF8CC541); + + // Colors for states + static const Color statusSuccess = Color(0xFF8CC541); + static const Color statusWarning = Color(0xFFF3CA29); + static const Color statusError = Color(0xFFE45A5A); + static const Color statusActive = Color(0xFF8CC541); + + // Padding and margin constants static const EdgeInsets smallPadding = EdgeInsets.all(8.0); static const EdgeInsets mediumPadding = EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0); @@ -29,7 +56,7 @@ class AppTheme { return ThemeData( hoverColor: dark1, primaryColor: mostroGreen, - scaffoldBackgroundColor: dark1, + scaffoldBackgroundColor: backgroundDark, appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, @@ -53,7 +80,7 @@ class AppTheme { textTheme: _buildTextTheme(), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - foregroundColor: AppTheme.cream1, + foregroundColor: cream1, backgroundColor: mostroGreen, textStyle: GoogleFonts.robotoCondensed( fontWeight: FontWeight.w500, @@ -103,10 +130,11 @@ class AppTheme { size: 24.0, ), listTileTheme: ListTileThemeData( - titleTextStyle: TextStyle( - color: grey, - fontFamily: GoogleFonts.robotoCondensed().fontFamily, - )), + titleTextStyle: TextStyle( + color: grey, + fontFamily: GoogleFonts.robotoCondensed().fontFamily, + ), + ), ); } @@ -115,47 +143,72 @@ class AppTheme { const TextTheme( displayLarge: TextStyle( fontSize: 24.0, - ), // For larger titles + ), displayMedium: TextStyle( fontWeight: FontWeight.w700, fontSize: 20.0, - ), // For medium titles + ), displaySmall: TextStyle( fontWeight: FontWeight.w700, fontSize: 16.0, - ), // For smaller titles + ), headlineMedium: TextStyle( fontWeight: FontWeight.w700, fontSize: 16.0, - ), // For subtitles + ), headlineSmall: TextStyle( fontWeight: FontWeight.w500, fontSize: 14.0, - ), // For secondary text + ), titleMedium: TextStyle( fontWeight: FontWeight.w500, fontSize: 16.0, - ), // For form labels + ), titleLarge: TextStyle( fontWeight: FontWeight.w500, fontSize: 18.0, - ), // For form labels + ), bodyLarge: TextStyle( fontWeight: FontWeight.w400, fontSize: 16.0, - ), // For body text + ), bodyMedium: TextStyle( fontWeight: FontWeight.w400, fontSize: 14.0, - ), // For smaller body text + ), labelLarge: TextStyle( fontWeight: FontWeight.w500, fontSize: 14.0, - ), // For buttons and labels + ), ), ).apply( bodyColor: cream1, displayColor: cream1, ); } + + // helpers for shadows + static List get cardShadow => [ + BoxShadow( + color: Colors.black.withOpacity(0.7), + blurRadius: 15, + offset: const Offset(0, 5), + spreadRadius: -3, + ), + BoxShadow( + color: Colors.white.withOpacity(0.07), + blurRadius: 1, + offset: const Offset(0, -1), + spreadRadius: 0, + ), + ]; + + static List get buttonShadow => [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 6, + offset: const Offset(0, 3), + spreadRadius: -2, + ), + ]; } diff --git a/lib/data/models/rating.dart b/lib/data/models/rating.dart index 3143a6eb..665c09e6 100644 --- a/lib/data/models/rating.dart +++ b/lib/data/models/rating.dart @@ -6,6 +6,7 @@ class Rating { final int lastRating; final int maxRate; final int minRate; + final int days; const Rating({ required this.totalReviews, @@ -13,6 +14,7 @@ class Rating { required this.lastRating, required this.maxRate, required this.minRate, + required this.days, }); factory Rating.deserialized(String data) { @@ -26,40 +28,88 @@ class Rating { try { final json = jsonDecode(data); - if (json is Map) { + + if (json is List && + json.length > 1 && + json[0] == 'rating' && + json[1] is Map) { + final Map ratingData = json[1] as Map; + return Rating( + totalReviews: _parseIntFromNestedJson(ratingData, 'total_reviews'), + totalRating: _parseDoubleFromNestedJson(ratingData, 'total_rating'), + days: _parseIntFromNestedJson(ratingData, 'days'), + lastRating: 0, + maxRate: 5, + minRate: 1, + ); + } else if (json is Map) { return Rating( totalReviews: _parseInt(json, 'total_reviews'), totalRating: _parseDouble(json, 'total_rating'), lastRating: _parseInt(json, 'last_rating'), maxRate: _parseInt(json, 'max_rate'), minRate: _parseInt(json, 'min_rate'), + days: _parseInt(json, 'days', defaultValue: 0), ); } else { - return Rating( - totalReviews: 0, - totalRating: (json[1]['total_reviews'] as int).toDouble(), - lastRating: 0, - maxRate: 0, - minRate: 0, - ); + return Rating.empty(); } } catch (e) { return Rating.empty(); } } - static int _parseInt(Map json, String field) { + static int _parseInt(Map json, String field, + {int defaultValue = 0}) { final value = json[field]; + if (value == null) return defaultValue; if (value is int) return value; if (value is double) return value.toInt(); - throw FormatException('Invalid value for $field: $value'); + try { + return int.parse(value.toString()); + } catch (_) { + return defaultValue; + } } - static double _parseDouble(Map json, String field) { + static int _parseIntFromNestedJson(Map json, String field, + {int defaultValue = 0}) { final value = json[field]; + if (value == null) return defaultValue; + if (value is int) return value; + if (value is double) return value.toInt(); + try { + return int.parse(value.toString()); + } catch (_) { + return defaultValue; + } + } + + static double _parseDouble(Map json, String field, + {double defaultValue = 0.0}) { + final value = json[field]; + if (value == null) return defaultValue; + if (value is double) return value; + if (value is int) return value.toDouble(); + try { + return double.parse(value.toString()); + } catch (_) { + return defaultValue; + } + } + + static double _parseDoubleFromNestedJson( + Map json, String field, + {double defaultValue = 0.0}) { + final value = json[field]; + if (value == null) return defaultValue; if (value is double) return value; if (value is int) return value.toDouble(); - throw FormatException('Invalid value for $field: $value'); + try { + return double.parse(value.toString()); + } catch (_) { + return defaultValue; + } } static Rating empty() { @@ -67,8 +117,9 @@ class Rating { totalReviews: 0, totalRating: 0.0, lastRating: 0, - maxRate: 0, - minRate: 0, + maxRate: 5, + minRate: 1, + days: 0, ); } } diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 1e9fd5ac..f41a16fe 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -5,8 +5,8 @@ import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/features/home/providers/home_order_providers.dart'; 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/mostro_app_drawer.dart'; @@ -15,170 +15,294 @@ class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Watch the filtered orders directly. final filteredOrders = ref.watch(filteredOrdersProvider); return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: const MostroAppBar(), + backgroundColor: AppTheme.backgroundDark, + appBar: _buildAppBar(), drawer: const MostroAppDrawer(), + // Creating a custom floating action button position + floatingActionButton: Padding( + padding: const EdgeInsets.only(bottom: 70), // Move above navbar + child: const AddOrderButton(), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, body: RefreshIndicator( onRefresh: () async { return await ref.refresh(filteredOrdersProvider); }, - child: Container( - margin: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.dark2, - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - _buildTabs(ref), - const SizedBox(height: 12.0), - _buildFilterButton(context, ref), - const SizedBox(height: 6.0), - Expanded( + child: Column( + children: [ + _buildTabs(ref), + _buildFilterButton(context, ref), + Expanded( + child: Container( + color: const Color(0xFF1D212C), child: filteredOrders.isEmpty ? const Center( - child: Text( - 'No orders available for this type', - style: TextStyle(color: AppTheme.cream1), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + color: Colors.white30, + size: 48, + ), + SizedBox(height: 16), + Text( + 'No orders available', + style: TextStyle( + color: Colors.white60, + fontSize: 16, + ), + ), + Text( + 'Try changing filter settings or check back later', + style: TextStyle( + color: Colors.white38, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], ), ) : ListView.builder( itemCount: filteredOrders.length, + padding: const EdgeInsets.only(bottom: 80, top: 6), itemBuilder: (context, index) { final order = filteredOrders[index]; - return OrderListItem( - order: order); + return OrderListItem(order: order); }, ), ), - const BottomNavBar(), - ], + ), + const BottomNavBar(), + ], + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: AppTheme.backgroundDark, + elevation: 0, + leadingWidth: 60, + toolbarHeight: 56, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container( + height: 1, + color: Colors.white.withOpacity(0.1), + ), + ), + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Builder( + builder: (context) => IconButton( + icon: const HeroIcon( + HeroIcons.bars3, + style: HeroIconStyle.outline, + color: Colors.white, + size: 24, + ), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, ), ), ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Stack( + alignment: Alignment.center, + children: [ + IconButton( + icon: const HeroIcon( + HeroIcons.bell, + style: HeroIconStyle.outline, + color: Colors.white, + size: 24, + ), + onPressed: () { + // Action for notifications + }, + ), + // Indicator for the number of notifications + Positioned( + top: 12, + right: 8, + child: Container( + width: 18, + height: 18, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Center( + child: Text( + '6', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ], ); } Widget _buildTabs(WidgetRef ref) { final orderType = ref.watch(homeOrderTypeProvider); + return Container( - decoration: const BoxDecoration( - color: AppTheme.dark1, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - )), + decoration: BoxDecoration( + color: AppTheme.backgroundDark, + border: Border( + bottom: BorderSide( + color: Colors.white.withOpacity(0.1), + width: 1.0, + ), + ), + ), child: Row( children: [ - Expanded( - child: GestureDetector( - onTap: () => ref.read(homeOrderTypeProvider.notifier).state = - OrderType.sell, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: orderType == OrderType.sell - ? AppTheme.dark2 - : AppTheme.dark1, - borderRadius: BorderRadius.only( - topLeft: - Radius.circular((orderType == OrderType.sell) ? 20 : 0), - topRight: - Radius.circular(orderType == OrderType.sell ? 20 : 0), - ), - ), - child: Text( - "BUY BTC", - textAlign: TextAlign.center, - style: TextStyle( - color: orderType == OrderType.sell - ? AppTheme.mostroGreen - : AppTheme.red1, - fontWeight: FontWeight.bold, - ), - ), + _buildTabButton( + ref, + "BUY BTC", + orderType == OrderType.sell, + OrderType.sell, + AppTheme.buyColor, + ), + _buildTabButton( + ref, + "SELL BTC", + orderType == OrderType.buy, + OrderType.buy, + AppTheme.sellColor, + ), + ], + ), + ); + } + + Widget _buildTabButton( + WidgetRef ref, + String text, + bool isActive, + OrderType type, + Color activeColor, + ) { + return Expanded( + child: InkWell( + onTap: () => ref.read(homeOrderTypeProvider.notifier).state = type, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isActive ? activeColor : Colors.transparent, + width: 3.0, // Thicker line ), ), ), - Expanded( - child: GestureDetector( - onTap: () => ref.read(homeOrderTypeProvider.notifier).state = - OrderType.buy, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: orderType == OrderType.buy - ? AppTheme.dark2 - : AppTheme.dark1, - borderRadius: BorderRadius.only( - topLeft: - Radius.circular((orderType == OrderType.buy) ? 20 : 0), - topRight: - Radius.circular(orderType == OrderType.buy ? 20 : 0), - ), - ), - child: Text( - "SELL BTC", - textAlign: TextAlign.center, - style: TextStyle( - color: orderType == OrderType.buy - ? AppTheme.mostroGreen - : AppTheme.red1, - fontWeight: FontWeight.bold, - ), - ), - ), + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: isActive ? activeColor : AppTheme.textInactive, + fontWeight: FontWeight.w600, // Semi-bold + fontSize: 15, + letterSpacing: 0.5, // Letter spacing + fontFamily: 'Roboto', // Assuming Roboto as font ), ), - ], + ), ), ); } Widget _buildFilterButton(BuildContext context, WidgetRef ref) { + final filteredOrders = ref.watch(filteredOrdersProvider); + return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - OutlinedButton.icon( - onPressed: () { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: OrderFilter(), - ); - }, - ); - }, - icon: const HeroIcon( - HeroIcons.funnel, - style: HeroIconStyle.outline, - color: AppTheme.cream1, - ), - label: const Text( - "FILTER", - style: TextStyle(color: AppTheme.cream1), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + // Usar el mismo color que el fondo de la lista para que se vea como una sola sección fluida + color: const Color(0xFF1D212C), + child: Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: OrderFilter(), + ); + }, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withOpacity(0.05)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: AppTheme.cream1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HeroIcon( + HeroIcons.funnel, + style: HeroIconStyle.outline, + color: Colors.white70, + size: 18, + ), + const SizedBox(width: 8), + const Text( + "FILTER", + style: 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.withOpacity(0.2), + ), + Text( + "${filteredOrders.length} offers", + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], ), ), - const SizedBox(width: 8), - Text( - "${ref.watch(filteredOrdersProvider).length} offers", - style: const TextStyle(color: AppTheme.cream1), - ), - ], + ), ), ); } diff --git a/lib/features/home/widgets/order_list_item.dart b/lib/features/home/widgets/order_list_item.dart index af4df320..054d30a2 100644 --- a/lib/features/home/widgets/order_list_item.dart +++ b/lib/features/home/widgets/order_list_item.dart @@ -2,13 +2,11 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/shared/providers/time_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; -import 'package:mostro_mobile/shared/widgets/custom_card.dart'; class OrderListItem extends ConsumerWidget { final NostrEvent order; @@ -19,160 +17,275 @@ class OrderListItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { ref.watch(timeProvider); - return GestureDetector( - onTap: () { - order.orderType == OrderType.buy - ? context.push('/take_buy/${order.orderId}') - : context.push('/take_sell/${order.orderId}'); - }, - child: CustomCard( - color: AppTheme.dark1, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(order.orderType == OrderType.buy ? 'buying' : 'selling'), - Text('${order.expiration}'), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - _getOrderOffering(context, order), - const SizedBox(width: 16), - ], - ), - const SizedBox(height: 8), - _buildPaymentMethod(context), - const SizedBox(height: 8), - Row( - children: [ - Text( - '${order.rating?.totalRating ?? 0.0} ${getStars(order.rating?.totalRating ?? 0.0)}'), - ], - ), - ], + // Determine if the premium is positive or negative for the color + final premiumValue = + order.premium != null ? double.tryParse(order.premium!) ?? 0.0 : 0.0; + final isPremiumPositive = premiumValue >= 0; + final premiumColor = + isPremiumPositive ? AppTheme.buyColor : AppTheme.sellColor; + final premiumText = premiumValue == 0 + ? "(0%)" + : isPremiumPositive + ? "(+$premiumValue%)" + : "($premiumValue%)"; + + return Container( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + decoration: BoxDecoration( + color: AppTheme.backgroundDark, + borderRadius: BorderRadius.circular(20), + boxShadow: AppTheme.cardShadow, + border: Border.all( + color: Colors.white.withOpacity(0.05), + width: 1, ), ), - ); - } + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + order.orderType == OrderType.buy + ? context.push('/take_buy/${order.orderId}') + : context.push('/take_sell/${order.orderId}'); + }, + highlightColor: Colors.white.withOpacity(0.05), + splashColor: Colors.white.withOpacity(0.03), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // First row: "SELLING" label and timestamp + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // SELLING/BUYING label with more contrast + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppTheme.backgroundCard, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.6), + blurRadius: 4, + offset: const Offset(0, 2), + spreadRadius: -1, + ), + BoxShadow( + color: Colors.white.withOpacity(0.08), + blurRadius: 1, + offset: const Offset(0, -1), + spreadRadius: 0, + ), + ], + ), + child: Text( + order.orderType == OrderType.buy ? 'BUYING' : 'SELLING', + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), - Widget _getOrderOffering(BuildContext context, NostrEvent order) { - return Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - children: [ - _buildStyledTextSpan( - context, - ' ', - '${order.fiatAmount}', - isValue: true, - isBold: true, + // Timestamp + Text( + order.expiration ?? '9 hours ago', + style: const TextStyle( + color: Colors.white60, + fontSize: 14, + ), + ), + ], ), - TextSpan( - text: - '${order.currency} ${CurrencyUtils.getFlagFromCurrency(order.currency!)} ', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontSize: 16.0, + ), + + // Second row: Amount and currency with flag and percentage + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + // Large amount with more contrast + Text( + order.fiatAmount.toString(), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + height: 1.1, + ), + ), + const SizedBox(width: 8), + + // Currency code and flag + Text( + '${order.currency ?? "CUP"} ', + style: const TextStyle( + fontSize: 18, + color: Colors.white, + ), + ), + Text( + () { + final String currencyCode = order.currency ?? 'CUP'; + return CurrencyUtils.getFlagFromCurrency( + currencyCode) ?? + ''; + }(), + style: const TextStyle(fontSize: 18), + ), + const SizedBox(width: 4), + + // Percentage with more vibrant color + Text( + premiumText, + style: TextStyle( + fontSize: 16, + color: premiumColor, + fontWeight: FontWeight.w600, ), + ), + ], + ), + ), + + // Third row: Payment method + Container( + margin: const EdgeInsets.fromLTRB(16, 8, 16, 8), + width: double.infinity, + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 14), + decoration: BoxDecoration( + color: AppTheme.backgroundCard, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.7), + blurRadius: 6, + offset: const Offset(0, 3), + spreadRadius: -2, + ), + BoxShadow( + color: Colors.white.withOpacity(0.08), + blurRadius: 1, + offset: const Offset(0, -1), + spreadRadius: 0, + ), + ], ), - TextSpan( - text: '(${order.premium}%)', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontSize: 16.0, + child: Row( + children: [ + // Emoji for payment method + const Text( + '💳 ', // Default emoji + style: TextStyle(fontSize: 16), + ), + Text( + order.paymentMethods.isNotEmpty + ? order.paymentMethods[0] + : 'tm', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, ), + ), + ], ), - ], - ), + ), + + // Fourth row: Rating with stars + Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 14), + decoration: BoxDecoration( + color: AppTheme.backgroundCard, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.7), + blurRadius: 6, + offset: const Offset(0, 3), + spreadRadius: -2, + ), + BoxShadow( + color: Colors.white.withOpacity(0.08), + blurRadius: 1, + offset: const Offset(0, -1), + spreadRadius: 0, + ), + ], + ), + child: _buildRatingRow(order), + ), + ], ), - ], + ), ), ); } - Widget _buildPaymentMethod(BuildContext context) { - String method = order.paymentMethods.isNotEmpty - ? order.paymentMethods[0] - : 'No payment method'; + Widget _buildRatingRow(NostrEvent order) { + final rating = order.rating?.totalRating ?? 0.0; + + final int reviews = order.rating?.totalReviews ?? 0; - String methods = order.paymentMethods.join('\n'); + final int daysOld = order.rating?.days ?? 0; return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: HeroIcon( - _getPaymentMethodIcon(method), - style: HeroIconStyle.outline, - color: AppTheme.cream1, - size: 16, - ), + Row( + children: [ + Text( + rating.toStringAsFixed(1), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + SizedBox( + height: 20, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: 5, + itemBuilder: (context, index) { + const Color starColor = Colors.amber; + + if (index < rating.floor()) { + return const Icon(Icons.star, color: starColor, size: 14); + } else if (index == rating.floor() && + rating - rating.floor() >= 0.5) { + return const Icon(Icons.star_half, + color: starColor, size: 14); + } else { + return Icon(Icons.star_border, + color: starColor.withOpacity(0.3), size: 14); + } + }), + ), + ], ), - const SizedBox(width: 4), - Flexible( - child: Text( - methods, - style: AppTheme.theme.textTheme.bodySmall, - overflow: TextOverflow.fade, - softWrap: true, + Text( + '$reviews reviews • $daysOld days old', + style: const TextStyle( + color: Colors.white60, + fontSize: 12, ), ), ], ); } - - HeroIcons _getPaymentMethodIcon(String method) { - switch (method.toLowerCase()) { - case 'wire transfer': - case 'transferencia bancaria': - return HeroIcons.buildingLibrary; - case 'revolut': - return HeroIcons.creditCard; - default: - return HeroIcons.banknotes; - } - } - - TextSpan _buildStyledTextSpan( - BuildContext context, - String label, - String value, { - bool isValue = false, - bool isBold = false, - }) { - return TextSpan( - text: label, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontWeight: FontWeight.normal, - fontSize: isValue ? 16.0 : 24.0, - ), - children: isValue - ? [ - TextSpan( - text: '$value ', - style: Theme.of(context).textTheme.displayLarge?.copyWith( - fontWeight: isBold ? FontWeight.bold : FontWeight.normal, - fontSize: 24.0, - color: AppTheme.cream1, - ), - ), - ] - : [], - ); - } - - String getStars(double count) { - return count > 0 ? '⭐' * count.toInt() : ''; - } } diff --git a/lib/shared/widgets/add_order_button.dart b/lib/shared/widgets/add_order_button.dart new file mode 100644 index 00000000..9101e343 --- /dev/null +++ b/lib/shared/widgets/add_order_button.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/core/app_theme.dart'; + +class AddOrderButton extends StatelessWidget { + const AddOrderButton({super.key}); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + onPressed: () => context.push('/add_order'), + backgroundColor: AppTheme.activeColor, + elevation: 6, + shape: const CircleBorder(), + child: const Icon( + Icons.add, + color: Colors.black, + size: 28, + ), + ); + } +} diff --git a/lib/shared/widgets/bottom_nav_bar.dart b/lib/shared/widgets/bottom_nav_bar.dart index d65bf1ce..b072ac5c 100644 --- a/lib/shared/widgets/bottom_nav_bar.dart +++ b/lib/shared/widgets/bottom_nav_bar.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:heroicons/heroicons.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:google_fonts/google_fonts.dart'; final chatCountProvider = StateProvider((ref) => 0); final orderBookNotificationCountProvider = StateProvider((ref) => 0); @@ -18,77 +19,107 @@ class BottomNavBar extends ConsumerWidget { ref.watch(orderBookNotificationCountProvider); return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Center( - child: Container( - height: 56, - width: 240, - decoration: BoxDecoration( - color: AppTheme.cream1, - borderRadius: BorderRadius.circular(28), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildNavItem( - context, - HeroIcons.bookOpen, - 0, - ), - _buildNavItem( - context, - HeroIcons.bookmarkSquare, - 1, - notificationCount: orderNotificationCount, - ), - _buildNavItem( - context, - HeroIcons.chatBubbleLeftRight, - 2, - notificationCount: chatCount, - ), - ], + width: double.infinity, + height: 80, + decoration: BoxDecoration( + color: AppTheme.backgroundNavBar, + border: Border( + top: BorderSide( + color: Colors.white.withOpacity(0.1), + width: 1, ), ), ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildNavItem( + context, + LucideIcons.book, + 'Order Book', + 0, + ), + _buildNavItem( + context, + LucideIcons.zap, + 'My Trades', + 1, + notificationCount: orderNotificationCount, + ), + _buildNavItem( + context, + LucideIcons.messageSquare, + 'Chat', + 2, + notificationCount: chatCount, + ), + ], + ), ); } - Widget _buildNavItem(BuildContext context, HeroIcons icon, int index, + Widget _buildNavItem( + BuildContext context, IconData icon, String label, int index, {int? notificationCount}) { bool isActive = _isActive(context, index); - return GestureDetector( - onTap: () => _onItemTapped(context, index), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: isActive ? const Color(0xFF8CC541) : Colors.transparent, - borderRadius: BorderRadius.circular(28), - ), - child: Stack( - clipBehavior: Clip.none, - children: [ - HeroIcon( - icon, - style: HeroIconStyle.outline, - color: Colors.black, - size: 24, - ), - if (notificationCount != null && notificationCount > 0) - // Position the red dot at the top left corner of the icon. - Positioned( - top: -2, - left: -2, - child: Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, + + Color iconColor = isActive ? AppTheme.activeColor : Colors.white; + Color textColor = isActive ? AppTheme.activeColor : Colors.white; + + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _onItemTapped(context, index), + child: SizedBox( + height: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 2), + SizedBox( + height: 24, + width: 24, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Icon( + icon, + color: iconColor, + size: 24, + ), + if (notificationCount != null && notificationCount > 0) + Positioned( + top: -2, + right: -2, + child: Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Text( + label, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w400, + color: textColor, + height: 1.0, + letterSpacing: -0.2, ), ), - ), - ], + const SizedBox(height: 2), + ], + ), + ), ), ), ); diff --git a/lib/shared/widgets/mostro_app_bar.dart b/lib/shared/widgets/mostro_app_bar.dart index 881b1e87..4d7e3047 100644 --- a/lib/shared/widgets/mostro_app_bar.dart +++ b/lib/shared/widgets/mostro_app_bar.dart @@ -1,6 +1,6 @@ +// lib/shared/widgets/mostro_app_bar.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; @@ -12,28 +12,63 @@ class MostroAppBar extends ConsumerWidget implements PreferredSizeWidget { return AppBar( backgroundColor: AppTheme.dark1, elevation: 0, - leading: IconButton( - icon: const HeroIcon(HeroIcons.bars3, - style: HeroIconStyle.outline, color: AppTheme.cream1), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ), - actions: [ - IconButton( - key: Key('createOrderButton'), - icon: const HeroIcon(HeroIcons.plus, - style: HeroIconStyle.outline, color: AppTheme.cream1), + leadingWidth: 70, + // Use a custom IconButton with specific padding + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: IconButton( + icon: const HeroIcon( + HeroIcons.bars3, + style: HeroIconStyle.outline, + color: AppTheme.cream1, + size: 28, + ), onPressed: () { - context.push('/add_order'); + Scaffold.of(context).openDrawer(); }, ), - IconButton( - icon: const HeroIcon(HeroIcons.bolt, - style: HeroIconStyle.solid, color: AppTheme.yellow), - onPressed: () async { - }, + ), + actions: [ + // Notification with count indicator + Stack( + children: [ + IconButton( + icon: const HeroIcon( + HeroIcons.bell, + style: HeroIconStyle.outline, + color: AppTheme.cream1, + size: 28, + ), + onPressed: () { + // Action for notifications + }, + ), + // Notification count indicator + Positioned( + top: 10, + right: 10, + child: Container( + width: 18, + height: 18, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Center( + child: Text( + '6', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], ), + const SizedBox(width: 16), // Spacing ], ); } diff --git a/pubspec.lock b/pubspec.lock index 22061f77..fb2bd2e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -881,6 +881,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + lucide_icons: + dependency: "direct main" + description: + name: lucide_icons + sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 + url: "https://pub.dev" + source: hosted + version: "0.257.0" macros: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9b0cae53..3bc50cf5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: line_icons: ^2.0.3 introduction_screen: ^3.1.17 riverpod_annotation: ^2.6.1 + lucide_icons: ^0.257.0 flutter_localizations: sdk: flutter