sojorn/sojorn_app/lib/widgets/reading_post_card.dart
2026-02-01 16:06:12 -06:00

339 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:timeago/timeago.dart' as timeago;
import '../models/post.dart';
import '../theme/app_theme.dart';
import 'media/signed_media_image.dart';
import '../routes/app_routes.dart';
class ReadingPostCard extends StatefulWidget {
final Post post;
final VoidCallback? onTap;
final VoidCallback? onSave;
final bool isSaved;
final bool showDivider;
const ReadingPostCard({
super.key,
required this.post,
this.onTap,
this.onSave,
this.isSaved = false,
this.showDivider = true,
});
@override
State<ReadingPostCard> createState() => _ReadingPostCardState();
}
class _ReadingPostCardState extends State<ReadingPostCard> {
bool _isPressed = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPostCard(),
if (widget.showDivider) _buildDivider(),
],
);
}
Widget _buildPostCard() {
return Container(
constraints: const BoxConstraints(maxWidth: 680),
margin: _getMargin(),
decoration: BoxDecoration(
color: AppTheme.white,
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
border: Border(
left: BorderSide(
color: _isPressed ? AppTheme.brightNavy : AppTheme.egyptianBlue,
width: AppTheme.flowLineWidth,
),
right: BorderSide(color: AppTheme.egyptianBlue, width: 1),
top: BorderSide(color: AppTheme.egyptianBlue, width: 1),
bottom: BorderSide(color: AppTheme.egyptianBlue, width: 1),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.fromLTRB(
AppTheme.spacingMd,
AppTheme.spacingSm,
AppTheme.spacingLg,
AppTheme.spacingMd,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAuthorRow(),
const SizedBox(height: AppTheme.spacingMd),
// White space area - clickable for post detail with full background coverage
InkWell(
onTap: widget.onTap,
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
splashColor: AppTheme.queenPink.withValues(alpha: 0.3),
highlightColor: Colors.transparent,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 4),
child: _buildBodyText(),
),
),
const SizedBox(height: AppTheme.spacingLg),
_buildActionRow(),
],
),
),
),
);
}
Widget _buildDivider() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppTheme.spacingMd),
child: Container(
height: AppTheme.dividerThickness,
decoration: BoxDecoration(color: AppTheme.egyptianBlue),
),
);
}
EdgeInsets _getMargin() {
final charCount = widget.post.body.length;
if (charCount < 100) {
return EdgeInsets.only(
left: AppTheme.spacingMd,
right: AppTheme.spacingMd,
top: AppTheme.spacingPostShort);
} else if (charCount < 300) {
return EdgeInsets.only(
left: AppTheme.spacingMd,
right: AppTheme.spacingMd,
top: AppTheme.spacingPostMedium);
}
return EdgeInsets.only(
left: AppTheme.spacingMd,
right: AppTheme.spacingMd,
top: AppTheme.spacingPostLong);
}
Widget _buildAuthorRow() {
final avatarUrl = widget.post.author?.avatarUrl;
final handle = widget.post.author?.handle ?? '';
final fallbackColor = _getAvatarColor(handle);
return InkWell(
onTap: () {
if (handle.isNotEmpty && handle != 'unknown') {
AppRoutes.navigateToProfile(context, handle);
}
},
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: fallbackColor,
borderRadius: BorderRadius.circular(10),
),
child: avatarUrl != null && avatarUrl.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(9),
child: SignedMediaImage(
url: avatarUrl,
width: 36,
height: 36,
fit: BoxFit.cover,
),
)
: Center(
child: Text(
handle.isNotEmpty ? handle[0].toUpperCase() : '?',
style: AppTheme.textTheme.labelMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(width: AppTheme.spacingSm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
widget.post.author?.displayName ?? 'Unknown',
style: AppTheme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w700,
color: AppTheme.navyBlue,
),
overflow: TextOverflow.ellipsis,
),
),
if (widget.post.author?.isOfficial == true) ...[
const SizedBox(width: AppTheme.spacingXs),
_buildOfficialBadge(),
],
if (widget.post.author?.isOfficial != true &&
widget.post.author?.trustState != null) ...[
const SizedBox(width: AppTheme.spacingXs),
_buildTrustBadge(),
],
],
),
const SizedBox(height: 2),
Text(
timeago.format(widget.post.createdAt),
style: AppTheme.textTheme.labelSmall
?.copyWith(color: AppTheme.egyptianBlue),
),
],
),
),
],
),
);
}
Widget _buildBodyText() {
final charCount = widget.post.body.length;
final style = charCount >= 10 ? AppTheme.postBodyLong : AppTheme.postBody;
return Text(widget.post.body, style: style);
}
Widget _buildActionRow() {
return Row(
children: [
if (widget.post.contentIntegrityScore < 1.0) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppTheme.spacingSm, vertical: 2),
decoration: BoxDecoration(
color: _getCISColor(widget.post.contentIntegrityScore)
.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(AppTheme.radiusXs),
),
child: Text(
'CIS ${(widget.post.contentIntegrityScore * 100).toInt()}%',
style: AppTheme.textTheme.labelSmall?.copyWith(
color: _getCISColor(widget.post.contentIntegrityScore),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: AppTheme.spacingMd),
],
const Spacer(),
_buildActionButton(
icon: widget.isSaved ? Icons.bookmark : Icons.bookmark_border,
isActive: widget.isSaved,
onPressed: widget.onSave,
color: AppTheme.brightNavy,
),
const SizedBox(width: AppTheme.spacingMd),
_buildActionButton(
icon: Icons.share_outlined,
isActive: false,
onPressed: null, // TODO: Implement share functionality
color: AppTheme.brightNavy,
),
],
);
}
Widget _buildActionButton({
required IconData icon,
required bool isActive,
VoidCallback? onPressed,
required Color color,
}) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppTheme.spacingSm, vertical: AppTheme.spacingXs),
child: Icon(icon,
size: 18, color: isActive ? color : AppTheme.royalPurple),
),
);
}
Widget _buildOfficialBadge() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: AppTheme.info.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(AppTheme.radiusXs),
),
child: Text('sojorn',
style: AppTheme.textTheme.labelSmall?.copyWith(
fontSize: 8,
fontWeight: FontWeight.w600,
color: AppTheme.info,
letterSpacing: 0.4,
)),
);
}
Widget _buildTrustBadge() {
final tier = widget.post.author!.trustState!.tier;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: _getTierColor(tier.value).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(AppTheme.radiusXs),
),
child: Text(tier.displayName.toUpperCase(),
style: AppTheme.textTheme.labelSmall?.copyWith(
fontSize: 8,
fontWeight: FontWeight.w600,
color: _getTierColor(tier.value),
letterSpacing: 0.4,
)),
);
}
Color _getAvatarColor(String handle) {
final hash = handle.hashCode;
final hue = (hash % 360).toDouble();
return HSLColor.fromAHSL(1.0, hue, 0.45, 0.55).toColor();
}
Color _getTierColor(String tier) {
switch (tier) {
case 'established':
return AppTheme.tierEstablished;
case 'trusted':
return AppTheme.tierTrusted;
default:
return AppTheme.tierNew;
}
}
Color _getCISColor(double cis) {
if (cis >= 0.8) return AppTheme.success;
if (cis >= 0.6) return AppTheme.warning;
return AppTheme.error;
}
}