From 94ffb419ae14df386f4df4860fa7e3700547ab75 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Sun, 1 Feb 2026 14:16:20 -0600 Subject: [PATCH] Replace appreciate button with smart reaction button - Remove appreciate button functionality (redundant with reactions) - Create SmartReactionButton that shows: - Plus icon when no reactions exist - Top reaction + count when reactions exist - User's reaction + count when user has reacted - Update ReactionPicker to show existing reactions first with counts - Add visual indicators for selected reactions and counts - Maintain full reaction functionality in single button - Improve UX by consolidating reaction interactions --- sojorn_app/lib/widgets/post/post_actions.dart | 67 ++++------- .../widgets/reactions/reaction_picker.dart | 61 ++++++++-- .../reactions/smart_reaction_button.dart | 104 ++++++++++++++++++ 3 files changed, 176 insertions(+), 56 deletions(-) create mode 100644 sojorn_app/lib/widgets/reactions/smart_reaction_button.dart diff --git a/sojorn_app/lib/widgets/post/post_actions.dart b/sojorn_app/lib/widgets/post/post_actions.dart index cd5d330..630a7b4 100644 --- a/sojorn_app/lib/widgets/post/post_actions.dart +++ b/sojorn_app/lib/widgets/post/post_actions.dart @@ -9,6 +9,7 @@ import '../../theme/app_theme.dart'; import '../sojorn_snackbar.dart'; import '../reactions/reaction_strip.dart'; import '../reactions/reaction_picker.dart'; +import '../reactions/smart_reaction_button.dart'; /// Post actions with a vibrant, clear, and energetic design. /// @@ -32,9 +33,7 @@ class PostActions extends ConsumerStatefulWidget { } class _PostActionsState extends ConsumerState { - late bool _isLiked; late bool _isSaved; - bool _isLiking = false; bool _isSaving = false; // Reaction state @@ -44,7 +43,6 @@ class _PostActionsState extends ConsumerState { @override void initState() { super.initState(); - _isLiked = widget.post.isLiked ?? false; _isSaved = widget.post.isSaved ?? false; _seedReactionState(); } @@ -65,37 +63,6 @@ class _PostActionsState extends ConsumerState { ); } - Future _toggleLike() async { - if (_isLiking) return; - setState(() { - _isLiking = true; - _isLiked = !_isLiked; - }); - - final apiService = ref.read(apiServiceProvider); - - try { - if (_isLiked) { - await apiService.appreciatePost(widget.post.id); - } else { - await apiService.unappreciatePost(widget.post.id); - } - } catch (e) { - if (mounted) { - setState(() { - _isLiked = !_isLiked; - }); - _showError(e.toString().replaceAll('Exception: ', '')); - } - } finally { - if (mounted) { - setState(() { - _isLiking = false; - }); - } - } - } - Future _toggleSave() async { if (_isSaving) return; setState(() { @@ -139,6 +106,18 @@ class _PostActionsState extends ConsumerState { } void _showReactionPicker() { + // Sort reactions: existing reactions first (by count), then common emojis + final existingReactions = _reactionCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + final allReactions = [ + ...existingReactions.map((e) => e.key), + '❤️', '👍', '😂', '😮', '😢', '😡', + '🎉', '🔥', '👏', '🙏', '💯', '🤔', + '😍', '🤣', '😊', '👌', '🙌', '💪', + '🎯', '⭐', '✨', '🌟', '💫', '☀️', + ]; + showDialog( context: context, builder: (context) => ReactionPicker( @@ -148,6 +127,9 @@ class _PostActionsState extends ConsumerState { onClosed: () { // Optional: Handle picker closed without selection }, + reactions: allReactions, + reactionCounts: _reactionCounts, + myReactions: _myReactions, ), ); } @@ -257,18 +239,11 @@ class _PostActionsState extends ConsumerState { ), ), if (allowChain) const SizedBox(width: 12), - IconButton( - onPressed: _isLiking ? null : _toggleLike, - icon: Icon( - _isLiked ? Icons.favorite : Icons.favorite_border, - color: _isLiked ? Colors.red : AppTheme.textSecondary, - ), - style: IconButton.styleFrom( - backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), + // Smart reaction button (replaces appreciate button) + SmartReactionButton( + reactionCounts: _reactionCounts, + myReactions: _myReactions, + onPressed: _showReactionPicker, ), const SizedBox(width: 8), IconButton( diff --git a/sojorn_app/lib/widgets/reactions/reaction_picker.dart b/sojorn_app/lib/widgets/reactions/reaction_picker.dart index 8a48552..d7b0363 100644 --- a/sojorn_app/lib/widgets/reactions/reaction_picker.dart +++ b/sojorn_app/lib/widgets/reactions/reaction_picker.dart @@ -5,11 +5,17 @@ import '../../theme/app_theme.dart'; class ReactionPicker extends StatefulWidget { final Function(String) onReactionSelected; final VoidCallback? onClosed; + final List? reactions; + final Map? reactionCounts; + final Set? myReactions; const ReactionPicker({ super.key, required this.onReactionSelected, this.onClosed, + this.reactions, + this.reactionCounts, + this.myReactions, }); @override @@ -26,6 +32,10 @@ class _ReactionPickerState extends State { @override Widget build(BuildContext context) { + final reactions = widget.reactions ?? _commonReactions; + final reactionCounts = widget.reactionCounts ?? {}; + final myReactions = widget.myReactions ?? {}; + return Dialog( backgroundColor: Colors.transparent, child: Container( @@ -85,9 +95,12 @@ class _ReactionPickerState extends State { mainAxisSpacing: 8, childAspectRatio: 1, ), - itemCount: _commonReactions.length, + itemCount: reactions.length, itemBuilder: (context, index) { - final emoji = _commonReactions[index]; + final emoji = reactions[index]; + final count = reactionCounts[emoji] ?? 0; + final isSelected = myReactions.contains(emoji); + return Material( color: Colors.transparent, child: InkWell( @@ -98,18 +111,46 @@ class _ReactionPickerState extends State { borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( - color: AppTheme.navyBlue.withValues(alpha: 0.05), + color: isSelected + ? AppTheme.brightNavy.withValues(alpha: 0.2) + : AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( - color: AppTheme.navyBlue.withValues(alpha: 0.1), - width: 1, + color: isSelected + ? AppTheme.brightNavy + : AppTheme.navyBlue.withValues(alpha: 0.1), + width: isSelected ? 2 : 1, ), ), - child: Center( - child: Text( - emoji, - style: const TextStyle(fontSize: 24), - ), + child: Stack( + children: [ + Center( + child: Text( + emoji, + style: const TextStyle(fontSize: 24), + ), + ), + if (count > 0) + Positioned( + right: 2, + bottom: 2, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: AppTheme.brightNavy, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + count > 99 ? '99+' : '$count', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], ), ), ), diff --git a/sojorn_app/lib/widgets/reactions/smart_reaction_button.dart b/sojorn_app/lib/widgets/reactions/smart_reaction_button.dart new file mode 100644 index 0000000..7b656d5 --- /dev/null +++ b/sojorn_app/lib/widgets/reactions/smart_reaction_button.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../theme/app_theme.dart'; + +class SmartReactionButton extends ConsumerWidget { + final Map reactionCounts; + final Set myReactions; + final VoidCallback onPressed; + + const SmartReactionButton({ + super.key, + required this.reactionCounts, + required this.myReactions, + required this.onPressed, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Determine what to show + if (myReactions.isNotEmpty) { + // Show user's reaction + total count + final myReaction = myReactions.first; + final totalCount = reactionCounts.values.fold(0, (a, b) => a + b); + + return IconButton( + onPressed: onPressed, + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + myReaction, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(width: 4), + Text( + totalCount > 99 ? '99+' : '$totalCount', + style: GoogleFonts.inter( + color: AppTheme.brightNavy, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + style: IconButton.styleFrom( + backgroundColor: AppTheme.brightNavy.withValues(alpha: 0.15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } else if (reactionCounts.isNotEmpty) { + // Show top reaction + total count + final topReaction = reactionCounts.entries + .reduce((a, b) => a.value > b.value ? a : b); + final totalCount = reactionCounts.values.fold(0, (a, b) => a + b); + + return IconButton( + onPressed: onPressed, + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + topReaction.key, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(width: 4), + Text( + totalCount > 99 ? '99+' : '$totalCount', + style: GoogleFonts.inter( + color: AppTheme.textSecondary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + style: IconButton.styleFrom( + backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } else { + // Show plus button + return IconButton( + onPressed: onPressed, + icon: Icon( + Icons.add_reaction_outlined, + color: AppTheme.textSecondary, + size: 20, + ), + style: IconButton.styleFrom( + backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } + } +}