import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:onufitness/constants/api_enum_constant.dart'; import 'package:onufitness/constants/asset_constants.dart'; import 'package:onufitness/constants/color_constant.dart'; import 'package:onufitness/constants/text_constant.dart'; import 'package:onufitness/screens/echoboard/widget/image_zoom.dart'; import 'package:onufitness/screens/echoboard/models/post_model.dart'; import 'package:onufitness/screens/u_vault/widgets/video_card.dart'; import 'package:onufitness/utils/custom_cache_manager.dart'; import 'package:onufitness/utils/custom_sneakbar.dart'; import 'package:onufitness/utils/helper_function.dart'; import 'package:shimmer/shimmer.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:intl/intl.dart'; import 'dart:developer' as developer; class PostCard extends StatefulWidget { final SocialPostItem post; final VoidCallback onLikePressed; final VoidCallback onLikeLongPressed; final VoidCallback onCommentPressed; final VoidCallback onSharePressed; final VoidCallback? onMorePressed; final VoidCallback? onProfilePressed; final VoidCallback? onPostTap; final Function(int pollId, int optionId)? onPollOptionSelected; final GlobalKey? likeButtonKey; final bool isTribeEchoboard; final bool isThreeDotButtonEnabled; const PostCard({ super.key, required this.post, required this.onLikePressed, required this.onCommentPressed, required this.onSharePressed, this.onMorePressed, this.onPollOptionSelected, required this.onLikeLongPressed, this.likeButtonKey, this.onProfilePressed, this.isTribeEchoboard = false, this.onPostTap, this.isThreeDotButtonEnabled = true, }); @override PostCardState createState() => PostCardState(); } class PostCardState extends State with AutomaticKeepAliveClientMixin { String? _selectedPollOptionId; //Do not remove _isVoting.... bool _isVoting = false; bool _isContentExpanded = false; static const int _contentPreviewLength = 150; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); // DON'T initialize video here - only set up poll state _initializePollState(); } void _initializePollState() { // Check if user has already voted on this poll if (_hasUserVotedOnAnyOption()) { // Set the selected option ID if user has already voted _setSelectedOptionFromUserVote(); } } @override void didUpdateWidget(PostCard oldWidget) { super.didUpdateWidget(oldWidget); // If the post data changed (especially poll data), we might need to reset UI states if (oldWidget.post.postId != widget.post.postId || oldWidget.post.poll?.pollPostId != widget.post.poll?.pollPostId) { // Reset voting state _isVoting = false; // Update selected option based on API data if (_hasUserVotedOnAnyOption()) { _setSelectedOptionFromUserVote(); } else { _selectedPollOptionId = null; } } // If reaction changed, we should update the UI as well if (oldWidget.post.postReactionTypeId != widget.post.postReactionTypeId) { developer.log( "Reaction type changed: ${oldWidget.post.postReactionTypeId} -> ${widget.post.postReactionTypeId}", ); } } @override void dispose() { super.dispose(); } // Check if the user has voted on any option in this poll bool _hasUserVotedOnAnyOption() { if (widget.post.poll?.options == null) return false; return widget.post.poll!.options!.any( (option) => option.hasUserVoted == true, ); } // Set the selected option ID from the user's vote void _setSelectedOptionFromUserVote() { if (widget.post.poll?.options != null) { for (var option in widget.post.poll!.options!) { if (option.hasUserVoted == true && option.optionId != null) { _selectedPollOptionId = option.optionId.toString(); break; } } } } bool _hasVideo() { return widget.post.mediaFiles?.any( (media) => media.mediaType?.toLowerCase().contains('video') ?? false, ) ?? false; } bool _hasImage() { return widget.post.mediaFiles?.any( (media) => media.mediaType?.toLowerCase().contains('image') ?? false, ) ?? false; } bool _hasPoll() { return widget.post.poll != null; } //............................................................................... // bool _isPollExpired() { // if (widget.post.poll?.expiryDate == null) return false; // return DateTime.now().isAfter(widget.post.poll!.expiryDate!); // } //............................................................................... String? _getFirstImageUrl() { if (!_hasImage()) return null; final imageMedia = widget.post.mediaFiles!.firstWhere( (media) => media.mediaType?.toLowerCase().contains('image') ?? false, orElse: () => MediaFile(), ); return imageMedia.mediaFile; } String? _getFirstVideoUrl() { if (!_hasVideo()) return null; final videoMedia = widget.post.mediaFiles!.firstWhere( (media) => media.mediaType?.toLowerCase().contains('video') ?? false, orElse: () => MediaFile(), ); return videoMedia.mediaFile; } String? _getFirstVideoThumbnail() { if (!_hasVideo()) return null; final videoMedia = widget.post.mediaFiles!.firstWhere( (media) => media.mediaType?.toLowerCase().contains('video') ?? false, orElse: () => MediaFile(), ); return videoMedia.thumbnailFile; } String _formatDateTime(DateTime? dateTime) { if (dateTime == null) return ''; final now = DateTime.now(); final difference = now.difference(dateTime); if (difference.inDays > 7) { return DateFormat('MMM d, yyyy').format(dateTime); } 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}min ago'; } else { return 'Just now'; } } void _votePoll(int optionId) async { developer.log("Vote poll function triggered for option: $optionId"); //............................................................................... // Check if user has already voted or poll has expired.................................... //............................................................................... // if (_hasUserVotedOnAnyOption()) { // developer.log("User already voted on this poll"); // customSnackbar( // title: "Voted", // message: "You have already voted on this poll", // duration: 1, // color: Colors.red.withValues(alpha: 0.7), // textColor: Colors.white, // ); // return; // } //............................................................................... // Checking if Poll is expired or not............................................. //............................................................................... // if (_isPollExpired()) { // customSnackbar( // title: "Expired", // message: "Poll expired, tou cannot vote now", // duration: 1, // color: Colors.red.withValues(alpha: 0.7), // textColor: Colors.white, // ); // return; // } //............................................................................... // Check if already votted that particular POLL Just now //............................................................................... // if (_isVoting) { // developer.log("Already in voting process"); // return; // } //............................................................................... // Just update the selected option immediately - no loading indicator or animation setState(() { _selectedPollOptionId = optionId.toString(); _isVoting = true; }); developer.log("Setting selected option: $_selectedPollOptionId"); try { // Call the API through the parent callback if (widget.onPollOptionSelected != null && widget.post.poll?.pollPostId != null) { developer.log( "Calling onPollOptionSelected callback with pollId: ${widget.post.poll!.pollPostId}, optionId: $optionId", ); // Call the API - the parent will le updating the post data widget.onPollOptionSelected!(widget.post.poll!.pollPostId!, optionId); } } catch (e) { // Reset if there's an error developer.log("Error during voting: $e"); setState(() { _selectedPollOptionId = null; _isVoting = false; }); customSnackbar( title: "Error", message: 'Failed to submit your vote. Please try after some time.', duration: 1, color: Colors.red.withValues(alpha: 0.7), textColor: Colors.white, ); } } //............................................................................... // Check if user has already voted or poll has expired.................................... //............................................................................... // bool _hasUserVoted() { // return _hasUserVotedOnAnyOption() || _isPollExpired(); // } //............................................................................... //............................................................................... // Check if content needs expansion bool _shouldShowSeeMore() { final content = widget.post.content; return content != null && content.length > _contentPreviewLength; } // Get content to display based on expansion state String _getDisplayContent() { final content = widget.post.content ?? ''; if (!_shouldShowSeeMore() || _isContentExpanded) { return content; } return content.substring(0, _contentPreviewLength); } Widget _buildProfileAvatar() { return InkWell( onTap: widget.onProfilePressed, child: ClipRRect( borderRadius: BorderRadius.circular(30.r), child: widget.post.profilePicture != null && widget.post.profilePicture!.trim().isNotEmpty ? CachedNetworkImage( imageUrl: widget.post.profilePicture!, width: 50.r, height: 50.r, fit: BoxFit.cover, filterQuality: FilterQuality.low, placeholder: (context, url) => buildAvatarPlaceholder(), errorWidget: (context, url, error) => buildAvatarPlaceholder(), ) : buildAvatarPlaceholder(), ), ); } Widget buildAvatarPlaceholder() { return CircleAvatar( backgroundColor: lightGreyColor, radius: isTablet ? 25.sp : 20.sp, child: Icon(Icons.person, size: 30, color: Color(darkGreyColor)), ); } Widget _getReactionIcon() { if (widget.post.postReactionTypeId == null) { return Image.asset( AssetConstants.likeIcon, height: isTablet ? 25.h : 20.h, width: isTablet ? 25.w : 20.w, ); } switch (widget.post.postReactionTypeId) { case ApiEnum.highFive: return Image.asset( AssetConstants.highFiveIcon, height: isTablet ? 25.h : 20.h, width: isTablet ? 25.w : 20.w, ); case ApiEnum.heart: return Image.asset( AssetConstants.heartReactionicon, height: isTablet ? 25.h : 20.h, width: isTablet ? 25.w : 20.w, ); case ApiEnum.beastMode: return Image.asset( AssetConstants.beastModeIcon, height: isTablet ? 25.h : 20.h, width: isTablet ? 25.w : 20.w, ); default: return Image.asset( AssetConstants.likeIcon, height: isTablet ? 25.h : 20.h, width: isTablet ? 25.w : 20.w, ); } } List parseTextWithBoldFormatting(String text) { List spans = []; RegExp boldRegex = RegExp(r'\*\*(.*?)\*\*'); int lastMatchEnd = 0; Iterable matches = boldRegex.allMatches(text); for (RegExpMatch match in matches) { if (match.start > lastMatchEnd) { spans.add( TextSpan( text: text.substring(lastMatchEnd, match.start), style: TextStyle(fontSize: smallSizeText, color: Colors.black), ), ); } spans.add( TextSpan( text: match.group(1), style: TextStyle( fontSize: smallSizeText, fontWeight: FontWeight.bold, color: Colors.black, ), ), ); lastMatchEnd = match.end; } if (lastMatchEnd < text.length) { spans.add( TextSpan( text: text.substring(lastMatchEnd), style: TextStyle(fontSize: smallSizeText, color: Colors.black), ), ); } return spans; } String getPostVisibilityText() { // Adjust these IDs based on your ApiEnum constants switch (widget.post.postVisibilityId) { case 1: // Public return '🌐 Public'; case 2: // Private return '🔒 Private'; case 3: // Custom return '⚙️ Exclusive'; default: return '🌐 Public'; // Default fallback } } // Build content with see more/see less functionality Widget _buildContentSection() { if (widget.post.content == null || widget.post.content!.isEmpty) { return SizedBox.shrink(); } return Padding( padding: EdgeInsets.all(12.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ RichText( text: TextSpan( children: parseTextWithBoldFormatting(_getDisplayContent()), ), ), if (_shouldShowSeeMore()) ...[ if (!_isContentExpanded) Text( '...', style: TextStyle(fontSize: smallSizeText, color: Colors.black), ), SizedBox(height: 4.h), GestureDetector( onTap: () { setState(() { _isContentExpanded = !_isContentExpanded; }); }, child: Text( _isContentExpanded ? 'See less' : 'See more', style: TextStyle( fontSize: smallSizeText, color: Color(primaryColor), fontWeight: FontWeight.w600, ), ), ), ], ], ), ); } @override Widget build(BuildContext context) { super.build(context); return InkWell( onTap: widget.onPostTap, child: Container( color: Colors.white, margin: EdgeInsets.only(bottom: 10.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( contentPadding: EdgeInsets.only(left: 10.w, right: 0), leading: _buildProfileAvatar(), title: InkWell( onTap: widget.onProfilePressed, child: Text( widget.post.fullName ?? 'Unknown User', style: TextStyle( fontSize: mediumSizeText, fontWeight: FontWeight.w600, ), ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.post.userTypeName != null ? widget.post.userTypeName == ApiEnum.coachUserRole && widget.post.coachTypeName != null ? Text( widget.post.coachTypeName!, style: TextStyle( fontSize: smallSizeText, fontWeight: FontWeight.w600, color: greyTextColor1, ), ) : Text( widget.post.userTypeName!, style: TextStyle( fontSize: smallSizeText, fontWeight: FontWeight.w600, color: greyTextColor1, ), ) : Container(), Row( children: [ Text( _formatDateTime(widget.post.createdAt), style: TextStyle( fontSize: 10.sp, fontWeight: FontWeight.w600, color: greyTextColor1, ), ), if (widget.post.postVisibilityId != null) ...[ if (widget.isTribeEchoboard == false) Text( ' • ', style: TextStyle( fontSize: verySmallSizeText, color: greyTextColor1, ), ), if (widget.isTribeEchoboard == false) Text( getPostVisibilityText(), style: TextStyle( fontSize: 10.sp, fontWeight: FontWeight.w500, color: greyTextColor1, ), ), ], ], ), ], ), trailing: widget.isThreeDotButtonEnabled ? IconButton( icon: Icon(Icons.more_vert), onPressed: widget.onMorePressed, ) : SizedBox.shrink(), ), // Content section with see more/see less _buildContentSection(), if (_hasImage()) GestureDetector( onTap: () { // Navigate to zoom view Navigator.of(context).push( MaterialPageRoute( builder: (context) => ImageZoomWidget( imageUrl: _getFirstImageUrl()!, heroTag: 'zoom_image_${widget.post.postId}${DateTime.now().millisecondsSinceEpoch}', ), ), ); }, child: Hero( tag: 'post_image_${widget.post.postId}${DateTime.now().millisecondsSinceEpoch}', child: CachedNetworkImage( imageUrl: _getFirstImageUrl()!, width: double.infinity, height: isTablet ? 300.h : 250.h, fit: BoxFit.cover, cacheManager: CustomCacheManager.instance, filterQuality: FilterQuality.low, placeholder: (context, url) => Shimmer.fromColors( baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100, child: Container( width: double.infinity, height: isTablet ? 300.h : 250.h, color: Colors.white, ), ), errorWidget: (context, url, error) => Container( height: isTablet ? 300.h : 250.h, color: Colors.grey[200], child: Center(child: Icon(Icons.error)), ), ), ), ), // Optimized video section - lazy loading with thumbnail if (_hasVideo()) VideoCard( category: "", videoUrl: _getFirstVideoUrl()!, isMyVideoScreen: false, title: null, thumbnailUrl: _getFirstVideoThumbnail(), ), if (_hasPoll()) Stack( children: [ Padding( padding: EdgeInsets.only( left: 12.w, right: 18.w, top: 10.w, bottom: 10.w, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( widget.post.poll!.question ?? 'Poll Question', style: TextStyle( fontSize: mediumSizeText, fontWeight: FontWeight.w600, ), ), ), if (widget.post.poll?.totalPollVotes != null) Text( '${widget.post.poll!.totalPollVotes} votes', style: TextStyle( fontSize: smallSizeText, color: Colors.grey[600], ), ), ], ), SizedBox(height: 12.h), if (widget.post.poll!.options != null) ...widget.post.poll!.options!.map( (option) => _buildPollOption(option), ), // Expired and Expiring date is showing............................................................................. // if (widget.post.poll!.expiryDate != null) // Padding( // padding: EdgeInsets.only(top: 12.h), // child: Row( // children: [ // Icon( // _isPollExpired() // ? Icons.lock_clock // : Icons.access_time, // size: 16.sp, // color: // _isPollExpired() // ? Colors.red // : Colors.grey[600], // ), // SizedBox(width: 4.w), // Text( // _isPollExpired() // ? 'Poll ended: ${_formatDateTime(widget.post.poll!.expiryDate)}' // : 'Poll ends: ${_formatDateTime(widget.post.poll!.expiryDate)}', // style: TextStyle( // fontSize: verySmallSizeText, // color: // _isPollExpired() // ? Colors.red // : Colors.grey[600], // ), // ), // ], // ), // ), //.............................................................................................. ], ), ), ], ), Padding( padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 8.h), child: Row( children: [ Row( children: [ IconButton( key: widget.likeButtonKey, icon: _getReactionIcon(), onPressed: () { widget.onLikePressed(); }, onLongPress: () { widget.onLikeLongPressed(); }, ), Text( '${widget.post.totalPostReactions ?? 0}', style: TextStyle( fontSize: smallSizeText, color: widget.post.postReactionTypeId != null ? Colors.black : null, ), ), ], ), SizedBox(width: 10.w), Row( children: [ IconButton( icon: Image.asset( AssetConstants.commentsLogo, height: 20.h, width: 20.w, ), onPressed: widget.onCommentPressed, ), Text( '${widget.post.totalPostComments ?? 0}', style: TextStyle(fontSize: smallSizeText), ), ], ), SizedBox(width: 10.w), Row( children: [ IconButton( icon: Image.asset( AssetConstants.shareLogo, height: 20.h, width: 20.w, ), onPressed: widget.onSharePressed, ), // Text('0', style: TextStyle(fontSize: smallSizeText)), ], ), SizedBox(width: 20.w), ], ), ), Divider(height: 1.h, thickness: 1.h, color: Colors.grey[200]), ], ), ), ); } Widget _buildPollOption(Option option) { final totalVotes = widget.post.poll?.totalPollVotes ?? 0; final optionVotes = option.totalOptionPollVotes ?? 0; double percentage = 0.0; if (totalVotes > 0 && optionVotes >= 0) { percentage = optionVotes / totalVotes; if (percentage.isNaN || percentage.isInfinite) { percentage = 0.0; } percentage = percentage.clamp(0.0, 1.0); } final bool isSelected = (option.optionId != null && _selectedPollOptionId == option.optionId.toString()) || (option.hasUserVoted == true && _selectedPollOptionId == null); //................................................................ // final isDisabled = _isPollExpired() || _hasUserVoted(); //................................................................ return Padding( padding: EdgeInsets.symmetric(vertical: 4.h), child: InkWell( //........ Checking Expired or not................................ // onTap: // isDisabled // ? null // : () { // developer.log("Poll option tapped: ${option.optionId}"); // if (option.optionId != null) { // _votePoll(option.optionId!); // } // }, //................................................................ onTap: () { developer.log("Poll option tapped: ${option.optionId}"); if (option.optionId != null) { _votePoll(option.optionId!); } }, borderRadius: BorderRadius.circular(8.r), child: Padding( padding: EdgeInsets.symmetric(vertical: 2.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( height: isTablet ? 45.h : 40.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.r), border: Border.all(color: containerBorderColor, width: 2), ), child: Stack( children: [ Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(6.r), child: LinearProgressIndicator( value: percentage, backgroundColor: Colors.white, valueColor: AlwaysStoppedAnimation( isSelected ? Color(primaryColor) : Color(primaryColor).withValues(alpha: 0.3), ), minHeight: isTablet ? 45.h : 40.h, ), ), ), // Content overlay Container( padding: EdgeInsets.symmetric(horizontal: 12.w), child: Center( child: Row( children: [ if (isSelected) ...[ Icon( Icons.check_circle, color: Colors.white, size: regularSizeText, ), SizedBox(width: 8.w), ], Expanded( child: Text( option.optionLabel ?? '', style: TextStyle( fontSize: smallSizeText, color: isSelected ? Colors.black : Colors.black87, fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( '${(percentage * 100).toInt()}% ($optionVotes)', style: TextStyle( fontSize: smallSizeText, color: isSelected ? Colors.black : Colors.black87, fontWeight: FontWeight.w500, ), ), ], ), ), ), ], ), ), ], ), ), ), ); } }