import { Heart, Download, Share2, User, MessageCircle, Edit3, Trash2, Maximize, Layers, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useState, useEffect } from "react"; import MarkdownRenderer from "@/components/MarkdownRenderer"; import EditImageModal from "@/components/EditImageModal"; import ImageLightbox from "@/components/ImageLightbox"; import MagicWizardButton from "@/components/MagicWizardButton"; import { downloadImage, generateFilename } from "@/utils/downloadUtils"; import { editImage } from "@/image-api"; import { usePostNavigation } from "@/contexts/PostNavigationContext"; import { useNavigate } from "react-router-dom"; import ResponsiveImage from "@/components/ResponsiveImage"; import { T, translate } from "@/i18n"; import { isLikelyFilename, formatDate } from "@/utils/textUtils"; import UserAvatarBlock from "@/components/UserAvatarBlock"; interface PhotoCardProps { pictureId: string; image: string; title: string; author: string; authorId: string; likes: number; comments: number; isLiked?: boolean; description?: string | null; onClick?: (pictureId: string) => void; onLike?: () => void; onDelete?: () => void; isVid?: boolean; onEdit?: (pictureId: string) => void; createdAt?: string; authorAvatarUrl?: string | null; showContent?: boolean; showHeader?: boolean; overlayMode?: 'hover' | 'always'; responsive?: any; variant?: 'grid' | 'feed'; apiUrl?: string; versionCount?: number; isExternal?: boolean; } const PhotoCard = ({ pictureId, image, title, author, authorId, likes, comments, isLiked = false, description, onClick, onLike, onDelete, isVid, onEdit, createdAt, authorAvatarUrl, showContent = true, showHeader = true, overlayMode = 'hover', responsive, variant = 'grid', apiUrl, versionCount, isExternal = false }: PhotoCardProps) => { const { user } = useAuth(); const navigate = useNavigate(); const { navigationData, setNavigationData, preloadImage } = usePostNavigation(); const [localIsLiked, setLocalIsLiked] = useState(isLiked); const [localLikes, setLocalLikes] = useState(likes); const [showEditModal, setShowEditModal] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [showLightbox, setShowLightbox] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [isPublishing, setIsPublishing] = useState(false); const [generatedImageUrl, setGeneratedImageUrl] = useState(null); const isOwner = user?.id === authorId; // Fetch version count for owners only const [localVersionCount, setLocalVersionCount] = useState(versionCount || 0); // Sync prop to state if needed, or just use prop. // If we want to allow local updates (e.g. after adding a version), we can keep state. useEffect(() => { if (versionCount !== undefined) { setLocalVersionCount(versionCount); } }, [versionCount]); // Legacy fetch removed in favor of passed prop /* useEffect(() => { // ... legacy fetch logic ... }, [pictureId, isOwner, user]); */ const handleLike = async (e: React.MouseEvent) => { e.stopPropagation(); if (isExternal) return; if (!user) { toast.error(translate('Please sign in to like pictures')); return; } try { if (localIsLiked) { // Unlike const { error } = await supabase .from('likes') .delete() .eq('user_id', user.id) .eq('picture_id', pictureId); if (error) throw error; setLocalIsLiked(false); setLocalLikes(prev => prev - 1); } else { // Like const { error } = await supabase .from('likes') .insert([{ user_id: user.id, picture_id: pictureId }]); if (error) throw error; setLocalIsLiked(true); setLocalLikes(prev => prev + 1); } onLike?.(); } catch (error) { console.error('Error toggling like:', error); toast.error(translate('Failed to update like')); } }; const handleDelete = async (e: React.MouseEvent) => { e.stopPropagation(); if (isExternal) return; if (!user || !isOwner) { toast.error(translate('You can only delete your own images')); return; } if (!confirm(translate('Are you sure you want to delete this image? This action cannot be undone.'))) { return; } setIsDeleting(true); try { // First get the picture details for storage cleanup const { data: picture, error: fetchError } = await supabase .from('pictures') .select('image_url') .eq('id', pictureId) .single(); if (fetchError) throw fetchError; // Delete from database (this will cascade delete likes and comments due to foreign keys) const { error: deleteError } = await supabase .from('pictures') .delete() .eq('id', pictureId); if (deleteError) throw deleteError; // Try to delete from storage as well if (picture?.image_url) { const urlParts = picture.image_url.split('/'); const fileName = urlParts[urlParts.length - 1]; const userIdFromUrl = urlParts[urlParts.length - 2]; const { error: storageError } = await supabase.storage .from('pictures') .remove([`${userIdFromUrl}/${fileName}`]); if (storageError) { console.error('Error deleting from storage:', storageError); // Don't show error to user as the main deletion succeeded } } toast.success(translate('Image deleted successfully')); onDelete?.(); // Trigger refresh of the parent component } catch (error) { console.error('Error deleting image:', error); toast.error(translate('Failed to delete image')); } finally { setIsDeleting(false); } }; const handleDownload = async () => { try { const filename = generateFilename(title); await downloadImage(image, filename); toast.success(translate('Image downloaded successfully')); } catch (error) { console.error('Error downloading image:', error); toast.error(translate('Failed to download image')); } }; const handleLightboxOpen = () => { // Update current index in navigation data if (navigationData) { const currentIndex = navigationData.posts.findIndex(p => p.id === pictureId); if (currentIndex !== -1) { setNavigationData({ ...navigationData, currentIndex }); } } setShowLightbox(true); }; const handleNavigate = (direction: 'prev' | 'next') => { if (!navigationData || !navigationData.posts.length) { toast.error(translate('No navigation data available')); return; } const newIndex = direction === 'next' ? navigationData.currentIndex + 1 : navigationData.currentIndex - 1; if (newIndex >= 0 && newIndex < navigationData.posts.length) { const newPost = navigationData.posts[newIndex]; setNavigationData({ ...navigationData, currentIndex: newIndex }); // Navigate to the new post navigate(`/post/${newPost.id}`); } else { toast.info(translate(direction === 'next' ? 'No next post available' : 'No previous post available')); } }; const handlePreload = (direction: 'prev' | 'next') => { if (!navigationData) return; const targetIndex = direction === 'next' ? navigationData.currentIndex + 1 : navigationData.currentIndex - 1; if (targetIndex >= 0 && targetIndex < navigationData.posts.length) { const targetPost = navigationData.posts[targetIndex]; preloadImage(targetPost.image_url); } }; const handlePromptSubmit = async (prompt: string) => { if (!prompt.trim()) { toast.error(translate('Please enter a prompt')); return; } setIsGenerating(true); try { // Convert image URL to File for API const response = await fetch(image); const blob = await response.blob(); const file = new File([blob], title || 'image.png', { type: blob.type || 'image/png' }); const result = await editImage(prompt, [file]); if (result) { // Convert ArrayBuffer to base64 data URL const uint8Array = new Uint8Array(result.imageData); const imageBlob = new Blob([uint8Array], { type: 'image/png' }); const reader = new FileReader(); reader.onload = () => { const dataUrl = reader.result as string; setGeneratedImageUrl(dataUrl); toast.success(translate('Image generated successfully!')); }; reader.readAsDataURL(imageBlob); } } catch (error) { console.error('Error generating image:', error); toast.error(translate('Failed to generate image. Please check your Google API key.')); } finally { setIsGenerating(false); } }; const handlePublish = async (option: 'overwrite' | 'new', imageUrl: string, newTitle: string, description?: string) => { if (isExternal) { toast.error(translate('Cannot publish external images')); return; } if (!user) { toast.error(translate('Please sign in to publish images')); return; } setIsPublishing(true); try { // Convert data URL to blob for upload const response = await fetch(imageUrl); const blob = await response.blob(); if (option === 'overwrite') { // Overwrite the existing image // First, get the current image URL to extract the file path const currentImageUrl = image; if (currentImageUrl.includes('supabase.co/storage/')) { const urlParts = currentImageUrl.split('/'); const fileName = urlParts[urlParts.length - 1]; const bucketPath = `${authorId}/${fileName}`; // Upload new image with same path (overwrite) const { error: uploadError } = await supabase.storage .from('pictures') .update(bucketPath, blob, { cacheControl: '0', // Disable caching to ensure immediate update upsert: true }); if (uploadError) throw uploadError; toast.success(translate('Image updated successfully!')); } else { toast.error(translate('Cannot overwrite this image')); return; } } else { // Create new image const fileName = `${user.id}/${Date.now()}-generated.png`; const { data: uploadData, error: uploadError } = await supabase.storage .from('pictures') .upload(fileName, blob); if (uploadError) throw uploadError; // Get public URL const { data: { publicUrl } } = supabase.storage .from('pictures') .getPublicUrl(fileName); // Save to database const { error: dbError } = await supabase .from('pictures') .insert([{ title: newTitle, description: description || translate('Generated from') + `: ${title}`, image_url: publicUrl, user_id: user.id }]); if (dbError) throw dbError; toast.success(translate('Image published to gallery!')); } setShowLightbox(false); setGeneratedImageUrl(null); // Trigger refresh if available onLike?.(); } catch (error) { console.error('Error publishing image:', error); toast.error(translate('Failed to publish image')); } finally { setIsPublishing(false); } }; const handleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onClick?.(pictureId); }; const handleCardClick = (e: React.MouseEvent) => { if (isVid) { handleLightboxOpen(); } else { handleClick(e); } }; return (
{/* Image */}
{/* Helper Badge for External Images */} {isExternal && (
External
)}
{/* Desktop Hover Overlay - hidden on mobile, and hidden in feed variant */} {showContent && variant === 'grid' && (
{showHeader && (
)}
{!isExternal && ( <> {localLikes > 0 && {localLikes}} {comments} )} {isOwner && !isExternal && ( <> {localVersionCount > 1 && (
{localVersionCount}
)} )}
{!isLikelyFilename(title) &&

{title}

} {description && (
)} {createdAt && (
{formatDate(createdAt)}
)}
{!isExternal && ( )}
)} {/* Mobile/Feed Content - always visible below image */} {showContent && (variant === 'feed' || (variant === 'grid' && true)) && (
{/* Row 1: User Avatar (Left) + Actions (Right) */}
{/* User Avatar Block */} {/* Actions */}
{!isExternal && ( <> {localLikes > 0 && ( {localLikes} )} {comments > 0 && ( {comments} )} )} {!isExternal && ( )} {isOwner && !isExternal && ( )}
{/* Likes */} {/* Caption / Description section */}
{(!isLikelyFilename(title) && title) && (
{title}
)} {description && (
)} {createdAt && (
{formatDate(createdAt)}
)}
)} {showEditModal && !isExternal && ( { setShowEditModal(false); onLike?.(); // Trigger refresh }} /> )} { setShowLightbox(false); setGeneratedImageUrl(null); }} imageUrl={generatedImageUrl || image} imageTitle={title} onPromptSubmit={handlePromptSubmit} onPublish={handlePublish} isGenerating={isGenerating} isPublishing={isPublishing} showPrompt={!isExternal} // Hide prompt/edit for external showPublish={!!generatedImageUrl && !isExternal} generatedImageUrl={generatedImageUrl || undefined} currentIndex={navigationData?.currentIndex} totalCount={navigationData?.posts.length} onNavigate={handleNavigate} onPreload={handlePreload} />
); }; export default PhotoCard;