mono/packages/ui/src/components/VersionSelector.tsx

307 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Check, Image as ImageIcon, Eye, EyeOff, Trash2 } from 'lucide-react';
import { fetchPictureById, fetchVersions, updatePicture, deletePictures, fetchUserPictures } from '@/modules/posts/client-pictures';
import { useAuth } from '@/hooks/useAuth';
import { toast } from 'sonner';
import { T, translate } from '@/i18n';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface Version {
id: string;
title: string;
image_url: string;
is_selected: boolean;
created_at: string;
parent_id: string | null;
visible: boolean;
user_id: string; // Added for storage deletion path
}
interface VersionSelectorProps {
currentPictureId: string;
onVersionSelect: (selectedVersionId: string) => void;
}
const VersionSelector: React.FC<VersionSelectorProps> = ({
currentPictureId,
onVersionSelect
}) => {
const { user } = useAuth();
const [versions, setVersions] = useState<Version[]>([]);
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState<string | null>(null);
const [toggling, setToggling] = useState<string | null>(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [versionToDelete, setVersionToDelete] = useState<Version | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
loadVersions();
}, [currentPictureId]);
const loadVersions = async () => {
if (!user || !currentPictureId) return;
setLoading(true);
try {
// Get the current picture to determine if it's a parent or child
const currentPicture = await fetchPictureById(currentPictureId);
if (!currentPicture) throw new Error('Picture not found');
// Fetch all versions via API
const allVersions = await fetchVersions(currentPicture, user.id);
setVersions(allVersions || []);
} catch (error) {
console.error('Error loading versions:', error);
toast.error(translate('Failed to load image versions'));
} finally {
setLoading(false);
}
};
const handleVersionSelect = async (versionId: string) => {
if (!user) return;
setUpdating(versionId);
try {
// Unselect all versions in this image tree, then select the chosen one
await Promise.all(
versions.map(v => updatePicture(v.id, { is_selected: v.id === versionId } as any))
);
// Update local state
setVersions(prevVersions =>
prevVersions.map(v => ({
...v,
is_selected: v.id === versionId
}))
);
toast.success(translate('Version selected successfully!'));
onVersionSelect(versionId);
} catch (error) {
console.error('Error selecting version:', error);
toast.error(translate('Failed to select version'));
} finally {
setUpdating(null);
}
};
const handleToggleVisibility = async (versionId: string, currentVisibility: boolean) => {
if (!user) return;
setToggling(versionId);
try {
await updatePicture(versionId, { visible: !currentVisibility } as any);
// Update local state
setVersions(prevVersions =>
prevVersions.map(v => ({
...v,
visible: v.id === versionId ? !currentVisibility : v.visible
}))
);
toast.success(translate(!currentVisibility ? 'Version made visible successfully!' : 'Version hidden successfully!'));
} catch (error) {
console.error('Error toggling visibility:', error);
toast.error(translate('Failed to update visibility'));
} finally {
setToggling(null);
}
};
const handleDeleteClick = (version: Version) => {
setVersionToDelete(version);
setShowDeleteDialog(true);
};
const confirmDelete = async () => {
if (!versionToDelete || !user) return;
setIsDeleting(true);
try {
// 1. Find all descendants to delete (cascade)
const allUserPictures = await fetchUserPictures(user.id);
const findDescendants = (parentId: string): any[] => {
const descendants: any[] = [];
const children = allUserPictures.filter((p: any) => p.parent_id === parentId);
children.forEach((child: any) => {
descendants.push(child);
descendants.push(...findDescendants(child.id));
});
return descendants;
};
const descendantsToDelete = findDescendants(versionToDelete.id);
const allToDelete = [versionToDelete, ...descendantsToDelete];
const idsToDelete = allToDelete.map(v => v.id);
// 2. Batch delete via API (handles storage + db)
await deletePictures(idsToDelete);
// 3. Update local state
const deletedIds = new Set(idsToDelete);
setVersions(prev => prev.filter(v => !deletedIds.has(v.id)));
const totalDeleted = allToDelete.length;
toast.success(translate(`Deleted ${totalDeleted > 1 ? `${totalDeleted} versions` : 'version'} successfully`));
} catch (error) {
console.error('Error deleting version:', error);
toast.error(translate('Failed to delete version'));
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
setVersionToDelete(null);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-4">
<div className="text-muted-foreground"><T>Loading versions...</T></div>
</div>
);
}
if (versions.length <= 1) {
return (
<div className="text-center p-4">
<p className="text-muted-foreground"><T>No other versions available for this image.</T></p>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<ImageIcon className="h-5 w-5" />
<h3 className="font-semibold"><T>Image Versions</T></h3>
<Badge variant="secondary">{versions.length}</Badge>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{versions.map((version) => (
<Card
key={version.id}
className={`cursor-pointer transition-all hover:scale-105 ${version.is_selected ? 'ring-2 ring-primary' : ''
}`}
>
<CardContent className="p-2">
<div className="aspect-square relative mb-2 overflow-hidden rounded-md">
<img
src={version.image_url}
alt={version.title}
className="w-full h-full object-cover"
/>
{version.is_selected && (
<div className="absolute top-2 right-2 bg-primary text-primary-foreground rounded-full p-1">
<Check className="h-3 w-3" />
</div>
)}
</div>
<div className="space-y-1">
<p className="text-xs font-medium truncate">{version.title}</p>
<p className="text-xs text-muted-foreground">
{new Date(version.created_at).toLocaleDateString()}
</p>
<div className="flex items-center gap-1">
{version.parent_id === null && (
<Badge variant="outline" className="text-xs"><T>Original</T></Badge>
)}
{!version.visible && (
<Badge variant="secondary" className="text-xs"><T>Hidden</T></Badge>
)}
</div>
</div>
<div className="flex gap-1 mt-2">
<Button
size="sm"
className="flex-1"
variant={version.is_selected ? "default" : "outline"}
onClick={() => handleVersionSelect(version.id)}
disabled={updating === version.id}
>
<T>{updating === version.id ? 'Selecting...' :
version.is_selected ? 'Selected' : 'Select'}</T>
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleVisibility(version.id, version.visible)}
disabled={toggling === version.id}
className="px-2"
>
{toggling === version.id ? (
<div className="animate-spin rounded-full h-3 w-3 border border-current border-t-transparent" />
) : version.visible ? (
<Eye className="h-3 w-3" />
) : (
<EyeOff className="h-3 w-3" />
)}
</Button>
<Button
size="sm"
variant="ghost"
className="px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleDeleteClick(version)}
disabled={isDeleting || version.id === currentPictureId} // Disable deleting the currently ACTIVE one? Or just handle correctly?
title={translate("Delete version")}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle><T>Delete Version</T></AlertDialogTitle>
<AlertDialogDescription>
<T>Are you sure you want to delete this version?</T> "{versionToDelete?.title}"
<br /><br />
<span className="text-destructive font-semibold">
<T>This action cannot be undone.</T>
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel><T>Cancel</T></AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
confirmDelete();
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isDeleting}
>
{isDeleting ? <T>Deleting...</T> : <T>Delete</T>}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
export default VersionSelector;