mono/packages/ui/src/components/feed/FeedCard.tsx
2026-01-20 10:34:09 +01:00

121 lines
4.3 KiB
TypeScript

import React, { useState } from 'react';
import { FeedPost } from '@/lib/db';
import { FeedCarousel } from './FeedCarousel';
import { Heart } from 'lucide-react';
import { cn } from '@/lib/utils';
import * as db from '@/lib/db';
import { useNavigate } from "react-router-dom";
import { normalizeMediaType } from "@/lib/mediaRegistry";
interface FeedCardProps {
post: FeedPost;
currentUserId?: string;
onLike?: () => void;
onComment?: () => void;
onShare?: () => void;
onNavigate?: (id: string) => void;
}
export const FeedCard: React.FC<FeedCardProps> = ({
post,
currentUserId,
onLike,
onComment,
onNavigate
}) => {
const navigate = useNavigate();
const [isLiked, setIsLiked] = useState<boolean>(false); // Need to hydrate this from props safely in real app
const [likeCount, setLikeCount] = useState(post.likes_count || 0);
const [lastTap, setLastTap] = useState<number>(0);
const [showHeartAnimation, setShowHeartAnimation] = useState(false);
// Initial check for like status (you might want to pass this in from parent if checking many)
React.useEffect(() => {
if (currentUserId && post.cover?.id) {
db.checkLikeStatus(currentUserId, post.cover.id).then(setIsLiked);
}
}, [currentUserId, post.cover?.id]);
const handleLike = async () => {
if (!currentUserId || !post.cover?.id) return;
// Optimistic update
const newStatus = !isLiked;
setIsLiked(newStatus);
setLikeCount(prev => newStatus ? prev + 1 : prev - 1);
try {
await db.toggleLike(currentUserId, post.cover.id, isLiked);
onLike?.();
} catch (e) {
// Revert
setIsLiked(!newStatus);
setLikeCount(prev => !newStatus ? prev + 1 : prev - 1);
console.error(e);
}
};
const handleDoubleTap = (e: React.SyntheticEvent) => {
const now = Date.now();
const DOUBLE_TAP_DELAY = 300;
if (now - lastTap < DOUBLE_TAP_DELAY) {
if (!isLiked) {
handleLike();
}
setShowHeartAnimation(true);
setTimeout(() => setShowHeartAnimation(false), 1000);
}
setLastTap(now);
};
// Prepare items for carousel
const carouselItems = (post.pictures && post.pictures.length > 0
? post.pictures
: [post.cover]).filter(item => !!item);
const handleItemClick = (itemId: string) => {
const item = carouselItems.find(i => i.id === itemId);
if (item) {
const type = normalizeMediaType(item.type);
if (type === 'page-intern' && item.meta?.slug) {
navigate(`/user/${item.user_id || post.user_id}/pages/${item.meta.slug}`);
return;
}
}
onNavigate?.(post.id);
};
if (carouselItems.length === 0) return null;
return (
<article className="bg-background border-b border-border pb-4 md:border md:rounded-lg md:mb-6 md:pb-0 overflow-hidden">
{/* Media Carousel */}
<div className="relative" onTouchEnd={handleDoubleTap} onClick={handleDoubleTap}>
<FeedCarousel
items={carouselItems}
aspectRatio={1}
className="w-full bg-muted"
author={post.author_profile?.display_name || post.author_profile?.username || 'User'}
authorId={post.user_id}
authorAvatarUrl={post.author_profile?.avatar_url}
onItemClick={handleItemClick}
/>
{/* Double tap heart animation overlay */}
<div className={cn(
"absolute inset-0 flex items-center justify-center pointer-events-none transition-opacity duration-300",
showHeartAnimation ? "opacity-100 scale-100" : "opacity-0 scale-50"
)}>
<Heart className="w-24 h-24 text-white fill-white drop-shadow-xl animate-bounce-short" />
</div>
</div>
{/* Actions Bar - Removed as actions are now per-item in the carousel */}
</article>
);
};