veza/apps/web/src/components/social/PostCard.tsx

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';