180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
import { Card } from '../ui/card';
|
|
import { Repeat } from 'lucide-react';
|
|
import { Post, Comment } from '../../types';
|
|
import { PostHeader } from './PostHeader';
|
|
import { PostContent } from './PostContent';
|
|
import { PostMedia } from './PostMedia';
|
|
import { PostFooterActions } from './PostFooterActions';
|
|
import { PostComments } from './PostComments';
|
|
import { SharePostModal } from './SharePostModal';
|
|
import toast from '@/utils/toast';
|
|
|
|
interface PostCardProps {
|
|
post: Post;
|
|
}
|
|
|
|
/** Format a timestamp string into a relative label like "2h ago" */
|
|
function formatRelativeTime(timestamp: string): string {
|
|
// If already relative (e.g. "2h", "1d", "10m"), normalise to "Xunit ago"
|
|
const relMatch = timestamp.match(/^(\d+)\s*(s|m|h|d|w|mo|y)$/i);
|
|
if (relMatch) {
|
|
const units: Record<string, string> = {
|
|
s: 's', m: 'm', h: 'h', d: 'd', w: 'w', mo: 'mo', y: 'y',
|
|
};
|
|
return `${relMatch[1]}${units[relMatch[2]?.toLowerCase() ?? ''] ?? ''} ago`;
|
|
}
|
|
// Try parsing as a date
|
|
const date = new Date(timestamp);
|
|
if (!isNaN(date.getTime())) {
|
|
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
if (seconds < 60) return 'just now';
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
const days = Math.floor(hours / 24);
|
|
if (days < 7) return `${days}d ago`;
|
|
const weeks = Math.floor(days / 7);
|
|
if (weeks < 5) return `${weeks}w ago`;
|
|
return date.toLocaleDateString();
|
|
}
|
|
return timestamp;
|
|
}
|
|
|
|
const PostCardComponent: React.FC<PostCardProps> = ({ post }) => {
|
|
const [isLiked, setIsLiked] = useState(post.isLiked || false);
|
|
const [likesCount, setLikesCount] = useState(post.likes);
|
|
const [likeAnimating, setLikeAnimating] = useState(false);
|
|
const [showComments, setShowComments] = useState(false);
|
|
const [showShareModal, setShowShareModal] = useState(false);
|
|
const [shareConfirmed, setShareConfirmed] = useState(false);
|
|
const likeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const shareTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (likeTimeoutRef.current) clearTimeout(likeTimeoutRef.current);
|
|
if (shareTimeoutRef.current) clearTimeout(shareTimeoutRef.current);
|
|
};
|
|
}, []);
|
|
|
|
// Mock comments
|
|
const comments: Comment[] = post.recentComments || [
|
|
{
|
|
id: 'c1',
|
|
author: {
|
|
name: 'Fan_01',
|
|
handle: '@fan',
|
|
avatar: 'https://picsum.photos/50',
|
|
},
|
|
content: 'This is fire! 🔥',
|
|
timestamp: '10m',
|
|
likes: 2,
|
|
},
|
|
{
|
|
id: 'c2',
|
|
author: {
|
|
name: 'Producer_X',
|
|
handle: '@pro_x',
|
|
avatar: 'https://picsum.photos/51',
|
|
},
|
|
content: 'What snare is that?',
|
|
timestamp: '30m',
|
|
likes: 5,
|
|
replies: [],
|
|
},
|
|
];
|
|
|
|
const handleLike = useCallback(() => {
|
|
setIsLiked((prev) => !prev);
|
|
setLikesCount((prev) => (isLiked ? prev - 1 : prev + 1));
|
|
if (!isLiked) {
|
|
setLikeAnimating(true);
|
|
if (likeTimeoutRef.current) clearTimeout(likeTimeoutRef.current);
|
|
likeTimeoutRef.current = setTimeout(() => setLikeAnimating(false), 400);
|
|
toast('Liked post');
|
|
}
|
|
}, [isLiked]);
|
|
|
|
const handleShare = useCallback(() => {
|
|
setShareConfirmed(true);
|
|
if (shareTimeoutRef.current) clearTimeout(shareTimeoutRef.current);
|
|
shareTimeoutRef.current = setTimeout(() => setShareConfirmed(false), 1500);
|
|
toast.success('Shared!');
|
|
}, []);
|
|
|
|
const handleShareConfirm = (type: 'repost' | 'quote', _text?: string) => {
|
|
toast.success(type === 'repost' ? 'Reposted!' : 'Quote posted!');
|
|
};
|
|
|
|
const relativeTimestamp = formatRelativeTime(post.timestamp);
|
|
|
|
return (
|
|
<>
|
|
<article>
|
|
<Card
|
|
variant="default"
|
|
className="p-0 overflow-hidden border-transparent hover:border-primary/20 hover:shadow-lg transition-all duration-[var(--sumi-duration-normal)] animate-fadeIn mb-4"
|
|
>
|
|
{/* Repost Header */}
|
|
{post.isRepost && (
|
|
<div className="px-4 pt-3 pb-0 flex items-center gap-2 text-xs text-muted-foreground font-bold uppercase tracking-wider">
|
|
<Repeat className="w-3 h-3" /> {post.repostAuthor} Reposted
|
|
</div>
|
|
)}
|
|
|
|
<PostHeader
|
|
author={post.author}
|
|
handle={post.author.handle}
|
|
timestamp={post.timestamp}
|
|
relativeTimestamp={relativeTimestamp}
|
|
/>
|
|
|
|
<PostContent content={post.content} tags={post.tags} />
|
|
|
|
<PostMedia
|
|
type={post.type}
|
|
image={post.image}
|
|
audioTrack={post.audioTrack}
|
|
pollOptions={post.pollOptions}
|
|
content={post.content}
|
|
/>
|
|
|
|
<PostFooterActions
|
|
isLiked={isLiked}
|
|
likesCount={likesCount}
|
|
commentsCount={post.comments}
|
|
sharesCount={post.shares}
|
|
likeAnimating={likeAnimating}
|
|
shareConfirmed={shareConfirmed}
|
|
onLike={handleLike}
|
|
onComment={() => setShowComments(!showComments)}
|
|
onRepost={() => setShowShareModal(true)}
|
|
onShare={handleShare}
|
|
/>
|
|
|
|
{showComments && (
|
|
<PostComments
|
|
comments={comments}
|
|
totalCommentsCount={post.comments}
|
|
onLikeComment={(_id) => toast('Liked comment')}
|
|
onReplyComment={(handle) => toast(`Replying to ${handle}`)}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</article>
|
|
|
|
{showShareModal && (
|
|
<SharePostModal
|
|
post={post}
|
|
onClose={() => setShowShareModal(false)}
|
|
onConfirm={handleShareConfirm}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const PostCard = React.memo(PostCardComponent);
|
|
PostCard.displayName = 'PostCard';
|