697 lines
22 KiB
Dart
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';
|
|
}
|
|
}
|
|
}
|