refactor: extract PostCardActions widget from PostCard

Extract action buttons (menu, share, comment, vote) into a separate
PostCardActions widget to reduce PostCard size and improve maintainability.

Changes:
- Create standalone PostCardActions widget
- Reduce PostCard from 504 to 249 lines
- Add comprehensive accessibility labels for all actions
- Maintain all existing functionality (voting, navigation, etc.)

Benefits:
- Better code organization and separation of concerns
- Meets 300-line widget limit requirement
- Easier to test action button behavior independently
- Improved screen reader support with dynamic Semantics labels

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+212
lib
+212
lib/widgets/post_card_actions.dart
···
···
+
import 'package:flutter/foundation.dart';
+
import 'package:flutter/material.dart';
+
import 'package:flutter/services.dart';
+
import 'package:provider/provider.dart';
+
+
import '../constants/app_colors.dart';
+
import '../models/post.dart';
+
import '../providers/auth_provider.dart';
+
import '../providers/vote_provider.dart';
+
import '../utils/date_time_utils.dart';
+
import 'icons/animated_heart_icon.dart';
+
import 'icons/reply_icon.dart';
+
import 'icons/share_icon.dart';
+
import 'sign_in_dialog.dart';
+
+
/// Action buttons row for post cards
+
///
+
/// Displays menu, share, comment, and like buttons with proper
+
/// authentication handling and optimistic updates.
+
class PostCardActions extends StatelessWidget {
+
const PostCardActions({required this.post, super.key});
+
+
final FeedViewPost post;
+
+
@override
+
Widget build(BuildContext context) {
+
return Row(
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
+
children: [
+
// Left side: Three dots menu and share
+
Row(
+
mainAxisSize: MainAxisSize.min,
+
children: [
+
// Three dots menu button
+
Semantics(
+
button: true,
+
label: 'Post options menu',
+
child: InkWell(
+
onTap: () {
+
// TODO: Show post options menu
+
if (kDebugMode) {
+
debugPrint('Menu button tapped for post');
+
}
+
},
+
child: Padding(
+
padding: const EdgeInsets.symmetric(
+
horizontal: 8,
+
vertical: 10,
+
),
+
child: Icon(
+
Icons.more_horiz,
+
size: 20,
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
+
),
+
),
+
),
+
),
+
+
// Share button
+
Semantics(
+
button: true,
+
label: 'Share post',
+
child: InkWell(
+
onTap: () {
+
// TODO: Handle share interaction with backend
+
if (kDebugMode) {
+
debugPrint('Share button tapped for post');
+
}
+
},
+
child: Padding(
+
padding: const EdgeInsets.symmetric(
+
horizontal: 8,
+
vertical: 10,
+
),
+
child: ShareIcon(
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
+
),
+
),
+
),
+
),
+
],
+
),
+
+
// Right side: Comment and heart
+
Row(
+
mainAxisSize: MainAxisSize.min,
+
children: [
+
// Comment button
+
Semantics(
+
button: true,
+
label:
+
'View ${post.post.stats.commentCount} ${post.post.stats.commentCount == 1 ? "comment" : "comments"}',
+
child: InkWell(
+
onTap: () {
+
// TODO: Navigate to post detail/comments screen
+
if (kDebugMode) {
+
debugPrint('Comment button tapped for post');
+
}
+
},
+
child: Padding(
+
padding: const EdgeInsets.symmetric(
+
horizontal: 12,
+
vertical: 10,
+
),
+
child: Row(
+
mainAxisSize: MainAxisSize.min,
+
children: [
+
ReplyIcon(
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
+
),
+
const SizedBox(width: 5),
+
Text(
+
DateTimeUtils.formatCount(post.post.stats.commentCount),
+
style: TextStyle(
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
+
fontSize: 13,
+
),
+
),
+
],
+
),
+
),
+
),
+
),
+
const SizedBox(width: 8),
+
+
// Heart button
+
Consumer<VoteProvider>(
+
builder: (context, voteProvider, child) {
+
final isLiked = voteProvider.isLiked(post.post.uri);
+
final adjustedScore = voteProvider.getAdjustedScore(
+
post.post.uri,
+
post.post.stats.score,
+
);
+
+
return Semantics(
+
button: true,
+
label:
+
isLiked
+
? 'Unlike post, $adjustedScore ${adjustedScore == 1 ? "like" : "likes"}'
+
: 'Like post, $adjustedScore ${adjustedScore == 1 ? "like" : "likes"}',
+
child: InkWell(
+
onTap: () async {
+
// Check authentication
+
final authProvider = context.read<AuthProvider>();
+
if (!authProvider.isAuthenticated) {
+
// Show sign-in dialog
+
final shouldSignIn = await SignInDialog.show(
+
context,
+
message: 'You need to sign in to like posts.',
+
);
+
+
if ((shouldSignIn ?? false) && context.mounted) {
+
// TODO: Navigate to sign-in screen
+
if (kDebugMode) {
+
debugPrint('Navigate to sign-in screen');
+
}
+
}
+
return;
+
}
+
+
// Light haptic feedback on both like and unlike
+
await HapticFeedback.lightImpact();
+
+
// Toggle vote with optimistic update
+
try {
+
await voteProvider.toggleVote(
+
postUri: post.post.uri,
+
postCid: post.post.cid,
+
);
+
} on Exception catch (e) {
+
if (kDebugMode) {
+
debugPrint('Failed to toggle vote: $e');
+
}
+
// TODO: Show error snackbar
+
}
+
},
+
child: Padding(
+
padding: const EdgeInsets.symmetric(
+
horizontal: 12,
+
vertical: 10,
+
),
+
child: Row(
+
mainAxisSize: MainAxisSize.min,
+
children: [
+
AnimatedHeartIcon(
+
isLiked: isLiked,
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
+
likedColor: const Color(0xFFFF0033),
+
),
+
const SizedBox(width: 5),
+
Text(
+
DateTimeUtils.formatCount(adjustedScore),
+
style: TextStyle(
+
color: AppColors.textPrimary.withValues(
+
alpha: 0.6,
+
),
+
fontSize: 13,
+
),
+
),
+
],
+
),
+
),
+
),
+
);
+
},
+
),
+
],
+
),
+
],
+
);
+
}
+
}