onufitness_mobile/lib/screens/home/widgets/top_five_post_section.dart
2026-01-13 11:36:24 +05:30

697 lines
22 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:onufitness/services/logger_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:onufitness/constants/color_constant.dart';
import 'package:onufitness/constants/text_constant.dart';
import 'package:onufitness/screens/echoboard/controllers/echoboard_controller.dart';
import 'package:onufitness/screens/echoboard/models/post_model.dart';
import 'package:onufitness/screens/echoboard/views/single_post_screen.dart';
import 'package:onufitness/screens/home/controllers/home_controller.dart';
import 'package:onufitness/screens/home/widgets/empty_data_widget.dart';
import 'package:onufitness/screens/navbar/bottom_nav_bar.dart';
import 'package:onufitness/utils/custom_sneakbar.dart';
class TopPostSection extends StatefulWidget {
final FitnessController controller;
final EchoBoardController echoBoardController;
final NavigationController navController;
const TopPostSection({
super.key,
required this.controller,
required this.echoBoardController,
required this.navController,
});
@override
State<TopPostSection> createState() => _TopPostSectionState();
}
class _TopPostSectionState extends State<TopPostSection> {
final logger = LoggerService();
void finalLoadPost(String postId) {
try {
widget.echoBoardController.loadSpecificPost(postId: postId);
return;
} catch (e) {
logger.error("Catch : Error getting EchoBoardController on attempt : $e");
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
SizedBox(height: 20.h),
Obx(() {
if (widget.controller.isTopPostLoading.value) {
return _buildLoadingState();
}
if (widget.controller.topPostData.value.data == null ||
widget.controller.topPostData.value.data!.items == null ||
widget.controller.topPostData.value.data!.items!.isEmpty) {
return emptyChartWidget();
}
return _buildPostsList();
}),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Top Performing Posts',
style: TextStyle(
fontSize: mediumSizeText,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 16.h),
Row(
children: [
Expanded(child: _buildDateButton(context, true)),
SizedBox(width: 12.w),
Icon(Icons.arrow_forward, size: 20.w, color: Colors.grey),
SizedBox(width: 12.w),
Expanded(child: _buildDateButton(context, false)),
],
),
SizedBox(height: 16.h),
],
);
}
Widget _buildDateButton(BuildContext context, bool isStartDate) {
return Obx(() {
DateTime date =
isStartDate
? widget.controller.topPostStartDate.value
: widget.controller.topPostEndDate.value;
return InkWell(
onTap: () => _selectDate(context, isStartDate),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Icon(Icons.calendar_today, size: 15.w, color: Colors.grey[600]),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isStartDate ? 'Start Date' : 'End Date',
style: TextStyle(
fontSize: verySmallSizeText,
color: Colors.grey[600],
),
),
Text(
DateFormat('MMM dd, yyyy').format(date),
style: TextStyle(
fontSize: verySmallSizeText,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
),
],
),
),
);
});
}
Future<void> _selectDate(BuildContext context, bool isStartDate) async {
DateTime initialDate =
isStartDate
? widget.controller.topPostStartDate.value
: widget.controller.topPostEndDate.value;
// Step 1: Pick the Date
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Color(primaryColor),
onPrimary: Colors.white,
),
),
child: child!,
);
},
);
if (pickedDate != null) {
// Step 2: Add current time automatically
final now = DateTime.now();
final DateTime fullPickedDate = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
now.hour,
now.minute,
now.second,
);
if (isStartDate) {
if (fullPickedDate.isBefore(widget.controller.topPostEndDate.value) ||
fullPickedDate.isAtSameMomentAs(
widget.controller.topPostEndDate.value,
)) {
widget.controller.updateTopPostDateRange(
fullPickedDate,
widget.controller.topPostEndDate.value,
);
} else {
customSnackbar(
title: 'Invalid Date Range',
message: 'Start date must be before end date',
duration: 2,
);
}
} else {
if (fullPickedDate.isAfter(widget.controller.topPostStartDate.value) ||
fullPickedDate.isAtSameMomentAs(
widget.controller.topPostStartDate.value,
)) {
widget.controller.updateTopPostDateRange(
widget.controller.topPostStartDate.value,
fullPickedDate,
);
} else {
customSnackbar(
title: 'Invalid Date Range',
message: 'End date must be after start date',
duration: 2,
);
}
}
}
}
Widget _buildLoadingState() {
return SizedBox(
height: 200.h,
child: Center(
child: CircularProgressIndicator(color: Color(primaryColor)),
),
);
}
Widget _buildPostsList() {
final posts = widget.controller.topPostData.value.data!.items!;
return Column(
children: List.generate(
posts.length,
(index) => InkWell(
onTap: () {
Get.to(
() => SinglePostPage(
postId: posts[index].postId.toString(),
tribeId: 0,
),
);
},
child: topFivePostCard(posts[index], index),
),
),
);
}
Widget topFivePostCard(dynamic post, int index) {
final hasMedia = post.mediaFiles != null && post.mediaFiles!.isNotEmpty;
final hasPoll = post.poll != null;
final hasContent = post.content != null && post.content!.isNotEmpty;
return Container(
margin: EdgeInsets.only(bottom: 16.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPostHeader(post, index),
if (hasContent) ...[SizedBox(height: 12.h), _buildPostContent(post)],
if (hasMedia) ...[
SizedBox(height: 12.h),
_buildMediaPreview(post.mediaFiles!),
],
if (hasPoll) ...[
SizedBox(height: 12.h),
_buildPollWidget(post.poll!),
],
SizedBox(height: 12.h),
_buildPostStats(post),
],
),
);
}
Widget _buildPostHeader(dynamic post, int index) {
return Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
decoration: BoxDecoration(
color: Color(primaryColor),
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'#${index + 1}',
style: TextStyle(
fontSize: verySmallSizeText,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
SizedBox(width: 12.w),
CircleAvatar(
radius: 20.r,
backgroundColor: Colors.grey[300],
backgroundImage:
post.profilePicture != null && post.profilePicture!.isNotEmpty
? CachedNetworkImageProvider(post.profilePicture!)
: null,
child:
post.profilePicture == null || post.profilePicture!.isEmpty
? Icon(Icons.person, size: 20.w, color: Colors.grey[600])
: null,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
post.fullName ?? 'Unknown User',
style: TextStyle(
fontSize: smallSizeText,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
if (post.userTypeName != null) ...[
SizedBox(height: 2.h),
Text(
post.userTypeName!,
style: TextStyle(
fontSize: verySmallSizeText,
color: Colors.grey[600],
),
),
],
],
),
),
if (post.createdAt != null)
Text(
_getTimeAgo(post.createdAt!),
style: TextStyle(
fontSize: verySmallSizeText,
color: Colors.grey[600],
),
),
],
);
}
Widget _buildPostContent(dynamic post) {
return Text(
post.content!,
style: TextStyle(
fontSize: smallSizeText,
color: Colors.black87,
height: 1.4,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
);
}
Widget _buildMediaPreview(List<MediaFile> mediaFiles) {
if (mediaFiles.isEmpty) return const SizedBox.shrink();
final displayCount = mediaFiles.length > 4 ? 4 : mediaFiles.length;
return SizedBox(
height: 200.h,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: displayCount == 1 ? 1 : 2,
crossAxisSpacing: 8.w,
mainAxisSpacing: 8.h,
childAspectRatio: displayCount == 1 ? 16 / 9 : 1,
),
itemCount: displayCount,
itemBuilder: (context, index) {
final media = mediaFiles[index];
final isVideo = media.mediaType?.toLowerCase() == 'video';
final imageUrl = media.mediaFile ?? '';
final videoThumbnail = media.thumbnailFile ?? '';
// If it's a video, prefer thumbnail
final displayUrl =
isVideo
? (videoThumbnail.isNotEmpty ? videoThumbnail : "")
: imageUrl;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
color: Colors.grey[300],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Stack(
fit: StackFit.expand,
children: [
if (displayUrl.isNotEmpty)
CachedNetworkImage(
imageUrl: displayUrl,
fit: BoxFit.cover,
placeholder:
(context, url) => Center(
child: CircularProgressIndicator(
color: Color(primaryColor),
strokeWidth: 2,
),
),
errorWidget:
(context, url, error) => Icon(
isVideo ? Icons.videocam : Icons.image,
color: Colors.grey[600],
size: 40.w,
),
)
else
Icon(
isVideo ? Icons.videocam : Icons.image,
color: Colors.grey[600],
size: 40.w,
),
if (isVideo)
Container(
color: Colors.black26,
child: Center(
child: Container(
padding: EdgeInsets.all(8.w),
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: Icon(
Icons.play_arrow,
color: Colors.white,
size: 32.w,
),
),
),
),
if (index == 3 && mediaFiles.length > 4)
Container(
color: Colors.black54,
child: Center(
child: Text(
'+${mediaFiles.length - 4}',
style: TextStyle(
color: Colors.white,
fontSize: largeSizeText,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
},
),
);
}
Widget _buildPollWidget(dynamic poll) {
final hasOptions = poll.options != null && poll.options!.isNotEmpty;
return Container(
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(primaryColor).withOpacity(0.05), Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Color(primaryColor).withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: Color(primaryColor).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.poll_rounded,
size: 18.w,
color: Color(primaryColor),
),
),
SizedBox(width: 10.w),
Expanded(
child: Text(
poll.question ?? 'Poll Question',
style: TextStyle(
fontSize: smallSizeText,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
],
),
if (hasOptions) ...[
SizedBox(height: 14.h),
...List.generate(poll.options!.length, (index) {
final option = poll.options![index];
final percentage = option.percentageOptionPollVotes ?? 0;
final hasVoted = option.hasUserVoted ?? false;
return Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
option.optionLabel ?? 'Option ${index + 1}',
style: TextStyle(
fontSize: smallSizeText,
color: Colors.black87,
fontWeight:
hasVoted ? FontWeight.w600 : FontWeight.w500,
),
),
),
SizedBox(width: 8.w),
Text(
'$percentage%',
style: TextStyle(
fontSize: smallSizeText,
fontWeight: FontWeight.w700,
color: Color(primaryColor),
),
),
],
),
SizedBox(height: 6.h),
Stack(
children: [
Container(
height: 8.h,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(4.r),
),
),
FractionallySizedBox(
widthFactor: percentage / 100,
child: Container(
height: 8.h,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(primaryColor),
Color(primaryColor).withOpacity(0.7),
],
),
borderRadius: BorderRadius.circular(4.r),
),
),
),
],
),
if (hasVoted) ...[
SizedBox(height: 4.h),
Row(
children: [
Icon(
Icons.check_circle,
size: 12.w,
color: Color(primaryColor),
),
SizedBox(width: 4.w),
Text(
'Your vote',
style: TextStyle(
fontSize: verySmallSizeText,
color: Color(primaryColor),
fontWeight: FontWeight.w600,
),
),
],
),
],
],
),
);
}),
],
SizedBox(height: 8.h),
Row(
children: [
Icon(Icons.how_to_vote, size: 14.w, color: Colors.grey[600]),
SizedBox(width: 6.w),
Text(
'${poll.totalPollVotes ?? 0} total votes',
style: TextStyle(
fontSize: verySmallSizeText,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildPostStats(dynamic post) {
return Row(
children: [
_buildStatItem(
Icons.favorite,
post.totalPostReactions?.toString() ?? '0',
'Reactions',
),
SizedBox(width: 20.w),
_buildStatItem(
Icons.chat_bubble,
post.totalPostComments?.toString() ?? '0',
'Comments',
),
],
);
}
Widget _buildStatItem(IconData icon, String count, String label) {
return Row(
children: [
Icon(icon, size: 16.w, color: Colors.grey[600]),
SizedBox(width: 6.w),
Text(
count,
style: TextStyle(
fontSize: verySmallSizeText,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(width: 4.w),
Text(
label,
style: TextStyle(
fontSize: verySmallSizeText,
color: Colors.grey[600],
),
),
],
);
}
String _getTimeAgo(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays > 365) {
return '${(difference.inDays / 365).floor()}y ago';
} else if (difference.inDays > 30) {
return '${(difference.inDays / 30).floor()}mo ago';
} else if (difference.inDays > 0) {
return '${difference.inDays}d ago';
} else if (difference.inHours > 0) {
return '${difference.inHours}h ago';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}m ago';
} else {
return 'Just now';
}
}
}