···
1
+
import 'package:cached_network_image/cached_network_image.dart';
2
+
import 'package:flutter/foundation.dart';
3
+
import 'package:flutter/material.dart';
5
+
import '../constants/app_colors.dart';
6
+
import '../models/post.dart';
7
+
import '../utils/date_time_utils.dart';
9
+
/// Post card widget for displaying feed posts
11
+
/// Displays a post with:
12
+
/// - Community and author information
13
+
/// - Post title and text content
14
+
/// - External embed (link preview with image)
15
+
/// - Action buttons (share, comment, like)
17
+
/// The [currentTime] parameter allows passing the current time for
18
+
/// time-ago calculations, enabling:
19
+
/// - Periodic updates of time strings
20
+
/// - Deterministic testing without DateTime.now()
21
+
class PostCard extends StatelessWidget {
22
+
const PostCard({required this.post, this.currentTime, super.key});
24
+
final FeedViewPost post;
25
+
final DateTime? currentTime;
28
+
Widget build(BuildContext context) {
30
+
margin: const EdgeInsets.only(bottom: 8),
31
+
decoration: const BoxDecoration(
32
+
color: AppColors.background,
33
+
border: Border(bottom: BorderSide(color: AppColors.border)),
36
+
padding: const EdgeInsets.fromLTRB(16, 4, 16, 1),
38
+
crossAxisAlignment: CrossAxisAlignment.start,
40
+
// Community and author info
43
+
// Community avatar placeholder
47
+
decoration: BoxDecoration(
48
+
color: AppColors.primary,
49
+
borderRadius: BorderRadius.circular(4),
53
+
post.post.community.name[0].toUpperCase(),
54
+
style: const TextStyle(
55
+
color: AppColors.textPrimary,
57
+
fontWeight: FontWeight.bold,
62
+
const SizedBox(width: 8),
65
+
crossAxisAlignment: CrossAxisAlignment.start,
68
+
'c/${post.post.community.name}',
69
+
style: const TextStyle(
70
+
color: AppColors.textPrimary,
72
+
fontWeight: FontWeight.bold,
76
+
'@${post.post.author.handle}',
77
+
style: const TextStyle(
78
+
color: AppColors.textSecondary,
87
+
DateTimeUtils.formatTimeAgo(
88
+
post.post.createdAt,
89
+
currentTime: currentTime,
92
+
color: AppColors.textPrimary.withValues(alpha: 0.5),
98
+
const SizedBox(height: 8),
101
+
if (post.post.title != null) ...[
104
+
style: const TextStyle(
105
+
color: AppColors.textPrimary,
107
+
fontWeight: FontWeight.w400,
112
+
// Spacing after title (only if we have content below)
113
+
if (post.post.title != null &&
114
+
(post.post.embed?.external != null ||
115
+
post.post.text.isNotEmpty))
116
+
const SizedBox(height: 8),
118
+
// Embed (link preview)
119
+
if (post.post.embed?.external != null) ...[
120
+
_EmbedCard(embed: post.post.embed!.external!),
121
+
const SizedBox(height: 8),
124
+
// Post text body preview
125
+
if (post.post.text.isNotEmpty) ...[
127
+
padding: const EdgeInsets.all(10),
128
+
decoration: BoxDecoration(
129
+
color: AppColors.backgroundSecondary,
130
+
borderRadius: BorderRadius.circular(8),
135
+
color: AppColors.textPrimary.withValues(alpha: 0.7),
140
+
overflow: TextOverflow.ellipsis,
145
+
// Reduced spacing before action buttons
146
+
const SizedBox(height: 4),
148
+
// Action buttons row
150
+
mainAxisAlignment: MainAxisAlignment.end,
155
+
// TODO: Handle share interaction with backend
157
+
debugPrint('Share button tapped for post');
161
+
// Increased padding for better touch targets
162
+
padding: const EdgeInsets.symmetric(
169
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
173
+
const SizedBox(width: 8),
178
+
// TODO: Navigate to post detail/comments screen
180
+
debugPrint('Comment button tapped for post');
184
+
// Increased padding for better touch targets
185
+
padding: const EdgeInsets.symmetric(
190
+
mainAxisSize: MainAxisSize.min,
193
+
Icons.chat_bubble_outline,
195
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
197
+
const SizedBox(width: 5),
199
+
DateTimeUtils.formatCount(
200
+
post.post.stats.commentCount,
203
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
211
+
const SizedBox(width: 8),
216
+
// TODO: Handle upvote/like interaction with backend
218
+
debugPrint('Heart button tapped for post');
222
+
// Increased padding for better touch targets
223
+
padding: const EdgeInsets.symmetric(
228
+
mainAxisSize: MainAxisSize.min,
231
+
Icons.favorite_border,
233
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
235
+
const SizedBox(width: 5),
237
+
DateTimeUtils.formatCount(post.post.stats.score),
239
+
color: AppColors.textPrimary.withValues(alpha: 0.6),
256
+
/// Embed card widget for displaying link previews
258
+
/// Shows a thumbnail image for external embeds with loading and error states.
259
+
class _EmbedCard extends StatelessWidget {
260
+
const _EmbedCard({required this.embed});
262
+
final ExternalEmbed embed;
265
+
Widget build(BuildContext context) {
266
+
// Only show image if thumbnail exists
267
+
if (embed.thumb == null) {
268
+
return const SizedBox.shrink();
272
+
decoration: BoxDecoration(
273
+
borderRadius: BorderRadius.circular(8),
274
+
border: Border.all(color: AppColors.border),
276
+
clipBehavior: Clip.antiAlias,
277
+
child: CachedNetworkImage(
278
+
imageUrl: embed.thumb!,
279
+
width: double.infinity,
283
+
(context, url) => Container(
284
+
width: double.infinity,
286
+
color: AppColors.background,
287
+
child: const Center(
288
+
child: CircularProgressIndicator(
289
+
color: AppColors.loadingIndicator,
293
+
errorWidget: (context, url, error) {
295
+
debugPrint('❌ Image load error: $error');
296
+
debugPrint('URL: $url');
299
+
width: double.infinity,
301
+
color: AppColors.background,
303
+
Icons.broken_image,
304
+
color: AppColors.loadingIndicator,