713 lines
23 KiB
TypeScript
713 lines
23 KiB
TypeScript
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<string | null>(null);
|
|
|
|
const isOwner = user?.id === authorId;
|
|
|
|
// Fetch version count for owners only
|
|
const [localVersionCount, setLocalVersionCount] = useState<number>(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 (
|
|
<div
|
|
data-testid="photo-card"
|
|
className="group relative overflow-hidden bg-card transition-all duration-300 cursor-pointer w-full"
|
|
onClick={handleCardClick}
|
|
>
|
|
{/* Image */}
|
|
<div className={`${variant === 'grid' ? "aspect-square" : ""} overflow-hidden`}>
|
|
<ResponsiveImage
|
|
src={image}
|
|
alt={title}
|
|
className="w-full h-full"
|
|
imgClassName="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
sizes={variant === 'grid'
|
|
? "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
: "100vw"
|
|
}
|
|
responsiveSizes={variant === 'grid' ? [320, 640, 1024, 1280] : [640, 1024, 1280, 1920]} // 1920 added
|
|
data={responsive}
|
|
apiUrl={apiUrl}
|
|
/>
|
|
{/* Helper Badge for External Images */}
|
|
{isExternal && (
|
|
<div className="absolute top-2 left-2 bg-black/60 text-white text-[10px] px-1.5 py-0.5 rounded flex items-center gap-1">
|
|
<ExternalLink className="w-3 h-3" />
|
|
External
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Desktop Hover Overlay - hidden on mobile, and hidden in feed variant */}
|
|
{showContent && variant === 'grid' && (
|
|
<div className={`hidden md:block absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent ${overlayMode === 'always' ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity duration-300 pointer-events-none`}>
|
|
<div className="absolute bottom-0 left-0 right-0 p-4 pointer-events-auto">
|
|
<div className="flex items-center justify-between mb-2">
|
|
{showHeader && (
|
|
<div className="flex items-center space-x-2">
|
|
<UserAvatarBlock
|
|
userId={authorId}
|
|
avatarUrl={authorAvatarUrl}
|
|
displayName={author}
|
|
hoverStyle={true}
|
|
showDate={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center space-x-1">
|
|
{!isExternal && (
|
|
<>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleLike}
|
|
className={`h-8 w-8 p-0 ${localIsLiked ? "text-red-500" : "text-white hover:text-red-500"
|
|
}`}
|
|
>
|
|
<Heart className="h-4 w-4" fill={localIsLiked ? "currentColor" : "none"} />
|
|
</Button>
|
|
{localLikes > 0 && <span className="text-white text-sm">{localLikes}</span>}
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-8 w-8 p-0 text-white hover:text-blue-400 ml-2"
|
|
>
|
|
<MessageCircle className="h-4 w-4" />
|
|
</Button>
|
|
<span className="text-white text-sm">{comments}</span>
|
|
</>
|
|
)}
|
|
|
|
{isOwner && !isExternal && (
|
|
<>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (onEdit) {
|
|
onEdit(pictureId);
|
|
} else {
|
|
setShowEditModal(true);
|
|
}
|
|
}}
|
|
className="h-8 w-8 p-0 text-white hover:text-green-400 ml-2"
|
|
>
|
|
<Edit3 className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleDelete}
|
|
disabled={isDeleting}
|
|
className="h-8 w-8 p-0 text-white hover:text-red-400 ml-2"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
|
|
{localVersionCount > 1 && (
|
|
<div className="flex items-center ml-2 px-2 py-1 bg-white/20 rounded text-white text-xs">
|
|
<Layers className="h-3 w-3 mr-1" />
|
|
{localVersionCount}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!isLikelyFilename(title) && <h3 className="text-white font-medium mb-1">{title}</h3>}
|
|
{description && (
|
|
<div className="text-white/80 text-sm mb-1 line-clamp-2 overflow-hidden">
|
|
<MarkdownRenderer content={description} className="prose-invert prose-white" />
|
|
</div>
|
|
)}
|
|
{createdAt && (
|
|
<div className="text-white/60 text-xs mb-2">
|
|
{formatDate(createdAt)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center space-x-1">
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
className="h-6 px-2 text-xs bg-white/20 hover:bg-white/30 border-0 text-white"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDownload();
|
|
}}
|
|
>
|
|
<Download className="h-3 w-3 mr-1" />
|
|
<T>Save</T>
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
className="h-6 w-6 p-0 bg-white/20 hover:bg-white/30 border-0 text-white"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleLightboxOpen();
|
|
}}
|
|
title="View in lightbox"
|
|
>
|
|
<Maximize className="h-2.5 w-2.5" />
|
|
</Button>
|
|
<Button size="sm" variant="secondary" className="h-6 w-6 p-0 bg-white/20 hover:bg-white/30 border-0 text-white">
|
|
<Share2 className="h-2.5 w-2.5" />
|
|
</Button>
|
|
{!isExternal && (
|
|
<MagicWizardButton
|
|
imageUrl={image}
|
|
imageTitle={title}
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 px-2 text-xs bg-white/20 hover:bg-white/30 border-0 text-white"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile/Feed Content - always visible below image */}
|
|
{showContent && (variant === 'feed' || (variant === 'grid' && true)) && (
|
|
<div className={`${variant === 'grid' ? "md:hidden" : ""} pb-2 space-y-2`}>
|
|
{/* Row 1: User Avatar (Left) + Actions (Right) */}
|
|
<div className="flex items-center justify-between px-2 pt-2">
|
|
{/* User Avatar Block */}
|
|
<UserAvatarBlock
|
|
userId={authorId}
|
|
avatarUrl={authorAvatarUrl}
|
|
displayName={author === 'User' ? undefined : author}
|
|
className="w-8 h-8"
|
|
showDate={false}
|
|
/>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-1">
|
|
{!isExternal && (
|
|
<>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
onClick={handleLike}
|
|
className={localIsLiked ? "text-red-500 hover:text-red-600" : ""}
|
|
>
|
|
<Heart className="h-6 w-6" fill={localIsLiked ? "currentColor" : "none"} />
|
|
</Button>
|
|
{localLikes > 0 && (
|
|
<span className="text-sm font-medium text-foreground mr-1">{localLikes}</span>
|
|
)}
|
|
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="text-foreground"
|
|
>
|
|
<MessageCircle className="h-6 w-6 -rotate-90" />
|
|
</Button>
|
|
{comments > 0 && (
|
|
<span className="text-sm font-medium text-foreground mr-1">{comments}</span>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="text-foreground"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDownload();
|
|
}}
|
|
>
|
|
<Download className="h-6 w-6" />
|
|
</Button>
|
|
|
|
{!isExternal && (
|
|
<MagicWizardButton
|
|
imageUrl={image}
|
|
imageTitle={title}
|
|
size="icon"
|
|
variant="ghost"
|
|
className="text-foreground hover:text-primary"
|
|
/>
|
|
)}
|
|
{isOwner && !isExternal && (
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (onEdit) {
|
|
onEdit(pictureId);
|
|
} else {
|
|
setShowEditModal(true);
|
|
}
|
|
}}
|
|
className="text-foreground hover:text-green-400"
|
|
>
|
|
<Edit3 className="h-6 w-6" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Likes */}
|
|
|
|
|
|
{/* Caption / Description section */}
|
|
<div className="px-4 space-y-1">
|
|
{(!isLikelyFilename(title) && title) && (
|
|
<div className="font-semibold text-sm">{title}</div>
|
|
)}
|
|
|
|
{description && (
|
|
<div className="text-sm text-foreground/90 line-clamp-3">
|
|
<MarkdownRenderer content={description} className="prose-sm dark:prose-invert" />
|
|
</div>
|
|
)}
|
|
|
|
{createdAt && (
|
|
<div className="text-xs text-muted-foreground pt-1">
|
|
{formatDate(createdAt)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showEditModal && !isExternal && (
|
|
<EditImageModal
|
|
open={showEditModal}
|
|
onOpenChange={setShowEditModal}
|
|
pictureId={pictureId}
|
|
currentTitle={title}
|
|
currentDescription={description}
|
|
currentVisible={true} // Default to true until we can properly pass this
|
|
onUpdateSuccess={() => {
|
|
setShowEditModal(false);
|
|
onLike?.(); // Trigger refresh
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<ImageLightbox
|
|
isOpen={showLightbox}
|
|
onClose={() => {
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PhotoCard; |