121 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
};
|