import { Heart, Download, Share2, User, MessageCircle, Edit3, Trash2, Maximize, Layers, ExternalLink } from "lucide-react"; import type { CardPreset } from "@/modules/pages/PageCard"; import { Button } from "@/components/ui/button"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import React, { useState, useEffect } from "react"; import MarkdownRenderer from "@/components/MarkdownRenderer"; const EditImageModal = React.lazy(() => import("@/components/EditImageModal")); const ImageLightbox = React.lazy(() => import("@/components/ImageLightbox")); import MagicWizardButton from "@/components/MagicWizardButton"; import { downloadImage, generateFilename } from "@/utils/downloadUtils"; 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"; import { toggleLike, deletePicture, uploadFileToStorage, createPicture, updateStorageFile } from "@/modules/posts/client-pictures"; 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; imageFit?: 'contain' | 'cover'; className?: string; // Allow custom classes from parent preset?: CardPreset; showAuthor?: boolean; showActions?: boolean; showTitle?: boolean; showDescription?: boolean; /** Place search: types[0], shown above description */ placeTypeLabel?: string | null; } 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, imageFit = 'contain', className, preset, showAuthor = true, showActions = true, showTitle, showDescription, placeTypeLabel }: PhotoCardProps) => { const { user } = useAuth(); const titleVisible = (showTitle ?? preset?.showTitle) !== false; const descriptionVisible = (showDescription ?? preset?.showDescription) !== false; const navigate = useNavigate(); 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]); const handleLike = async (e: React.MouseEvent) => { e.stopPropagation(); if (isExternal) return; if (!user) { toast.error(translate('Please sign in to like pictures')); return; } try { const nowLiked = await toggleLike(user.id, pictureId, localIsLiked); setLocalIsLiked(nowLiked); setLocalLikes(prev => nowLiked ? prev + 1 : 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 { await deletePicture(pictureId); 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 = () => { setShowLightbox(true); }; 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 { editImage } = await import("@/image-api"); 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 const currentImageUrl = image; if (currentImageUrl.includes('supabase.co/storage/')) { const urlParts = currentImageUrl.split('/'); const fileName = urlParts[urlParts.length - 1]; const bucketPath = `${authorId}/${fileName}`; await updateStorageFile(bucketPath, blob); toast.success(translate('Image updated successfully!')); } else { toast.error(translate('Cannot overwrite this image')); return; } } else { // Create new image const publicUrl = await uploadFileToStorage(user.id, blob); await createPicture({ title: newTitle, description: description || translate('Generated from') + `: ${title}`, image_url: publicUrl, user_id: user.id } as any); 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); } }; // console.log(preset, variant); return (
{/* Image */}
{/* Helper Badge for External Images */} {isExternal && (
External
)}
{variant === 'grid' && (title || description || placeTypeLabel) && (
{titleVisible && title && !isLikelyFilename(title) && (

{title}

)} {placeTypeLabel && (

{placeTypeLabel}

)} {descriptionVisible && description && (

{description}

)}
)} {/* TESTING: Entire desktop hover overlay disabled */} {false && showContent && variant === 'grid' && (
{showHeader && showAuthor && (
)}
{showActions && !isExternal && ( <> {localLikes > 0 && {localLikes}} {comments} )} {isOwner && !isExternal && ( <> {localVersionCount > 1 && (
{localVersionCount}
)} )}
{titleVisible && !isLikelyFilename(title) &&

{title}

} {descriptionVisible && description && (
)} {createdAt && (
{formatDate(createdAt)}
)} {showActions && (
{!isExternal && ( )}
)}
)} {/* Mobile/Feed Content - always visible below image */} {showContent && (variant === 'feed' || (variant === 'grid' && true)) && (
{/* Row 1: User Avatar (Left) + Actions (Right) */}
{/* User Avatar Block */} {showAuthor && ( )} {/* Actions */} {showActions && (
{!isExternal && ( <> {localLikes > 0 && ( {localLikes} )} {comments > 0 && ( {comments} )} )} {!isExternal && ( )} {isOwner && !isExternal && ( )}
)}
{/* Likes */} {/* Caption / Description section */}
{titleVisible && (!isLikelyFilename(title) && title) && (
{title}
)} {placeTypeLabel && (
{placeTypeLabel}
)} {descriptionVisible && 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} showPublish={!!generatedImageUrl && !isExternal} generatedImageUrl={generatedImageUrl || undefined} />
); }; export default PhotoCard;