Files
thrilltrack-explorer/src-old/components/upload/PhotoManagementDialog.tsx

411 lines
13 KiB
TypeScript

import { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { useToast } from '@/hooks/use-toast';
import { Trash2, Pencil } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { getErrorMessage } from '@/lib/errorHandler';
interface Photo {
id: string;
cloudflare_image_id: string;
cloudflare_image_url: string;
title: string | null;
caption: string | null;
order_index: number;
is_featured: boolean;
}
interface PhotoManagementDialogProps {
entityId: string;
entityType: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onUpdate?: () => void;
}
export function PhotoManagementDialog({
entityId,
entityType,
open,
onOpenChange,
onUpdate,
}: PhotoManagementDialogProps) {
const [photos, setPhotos] = useState<Photo[]>([]);
const [loading, setLoading] = useState(false);
const [editingPhoto, setEditingPhoto] = useState<Photo | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [photoToDelete, setPhotoToDelete] = useState<Photo | null>(null);
const [deleteReason, setDeleteReason] = useState('');
const { toast } = useToast();
useEffect(() => {
if (open) {
fetchPhotos();
}
}, [open, entityId, entityType]);
const fetchPhotos = async () => {
setLoading(true);
try {
const { data, error } = await supabase
.from('photos')
.select('id, cloudflare_image_id, cloudflare_image_url, title, caption, order_index, is_featured')
.eq('entity_type', entityType)
.eq('entity_id', entityId)
.order('order_index', { ascending: true });
if (error) throw error;
setPhotos((data || []).map(p => ({
...p,
order_index: p.order_index ?? 0,
is_featured: p.is_featured ?? false
})) as Photo[]);
} catch (error: unknown) {
toast({
title: 'Error',
description: getErrorMessage(error),
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleDeleteClick = (photo: Photo) => {
setPhotoToDelete(photo);
setDeleteReason('');
setDeleteDialogOpen(true);
};
const requestPhotoDelete = async () => {
if (!photoToDelete || !deleteReason.trim()) return;
try {
// Get current user
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
// Fetch entity name from database based on entity type
let entityName = 'Unknown';
try {
if (entityType === 'park') {
const { data } = await supabase.from('parks').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
} else if (entityType === 'ride') {
const { data } = await supabase.from('rides').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
} else if (entityType === 'ride_model') {
const { data } = await supabase.from('ride_models').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(entityType)) {
const { data } = await supabase.from('companies').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
}
} catch {
// Failed to fetch entity name - use default
}
// Create content submission
const { data: submission, error: submissionError } = await supabase
.from('content_submissions')
.insert([{
user_id: user.id,
submission_type: 'photo_delete',
content: {
action: 'delete',
photo_id: photoToDelete.id
}
}])
.select()
.single();
if (submissionError) throw submissionError;
// Create submission item with all necessary data
const { error: itemError } = await supabase
.from('submission_items')
.insert({
submission_id: submission.id,
item_type: 'photo_delete',
item_data: {
photo_id: photoToDelete.id,
cloudflare_image_id: photoToDelete.cloudflare_image_id,
entity_type: entityType,
entity_id: entityId,
entity_name: entityName,
cloudflare_image_url: photoToDelete.cloudflare_image_url,
title: photoToDelete.title,
caption: photoToDelete.caption,
deletion_reason: deleteReason
},
status: 'pending'
});
if (itemError) throw itemError;
toast({
title: 'Delete request submitted',
description: 'Your photo deletion request has been submitted for moderation',
});
setDeleteDialogOpen(false);
setPhotoToDelete(null);
setDeleteReason('');
onOpenChange(false);
} catch (error: unknown) {
toast({
title: 'Error',
description: getErrorMessage(error),
variant: 'destructive',
});
}
};
const requestPhotoEdit = async () => {
if (!editingPhoto) return;
try {
// Get current user
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
// Get original photo data
const originalPhoto = photos.find(p => p.id === editingPhoto.id);
if (!originalPhoto) throw new Error('Original photo not found');
// Create content submission
const { data: submission, error: submissionError } = await supabase
.from('content_submissions')
.insert([{
user_id: user.id,
submission_type: 'photo_edit',
content: {
action: 'edit',
photo_id: editingPhoto.id
}
}])
.select()
.single();
if (submissionError) throw submissionError;
// Create submission item
const { error: itemError } = await supabase
.from('submission_items')
.insert({
submission_id: submission.id,
item_type: 'photo_edit',
item_data: {
photo_id: editingPhoto.id,
entity_type: entityType,
entity_id: entityId,
new_caption: editingPhoto.caption,
cloudflare_image_url: editingPhoto.cloudflare_image_url,
},
original_data: {
caption: originalPhoto.caption,
},
status: 'pending'
});
if (itemError) throw itemError;
setEditingPhoto(null);
toast({
title: 'Edit request submitted',
description: 'Your photo edit has been submitted for moderation',
});
onOpenChange(false);
} catch (error: unknown) {
toast({
title: 'Error',
description: getErrorMessage(error),
variant: 'destructive',
});
}
};
if (editingPhoto) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Photo</DialogTitle>
<DialogDescription>Update photo caption</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="aspect-video w-full overflow-hidden rounded-lg">
<img
src={editingPhoto.cloudflare_image_url}
alt={editingPhoto.caption || 'Photo'}
className="w-full h-full object-cover"
/>
</div>
<div className="space-y-2">
<Label htmlFor="caption">Caption</Label>
<Textarea
id="caption"
value={editingPhoto.caption || ''}
onChange={(e) =>
setEditingPhoto({ ...editingPhoto, caption: e.target.value })
}
placeholder="Photo caption"
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditingPhoto(null)}>
Cancel
</Button>
<Button onClick={requestPhotoEdit}>Submit for Review</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl lg:max-w-4xl max-h-[80vh] overflow-y-auto p-3 sm:p-6">
<DialogHeader>
<DialogTitle>Manage Photos</DialogTitle>
<DialogDescription>
Edit or delete photos for this entity
</DialogDescription>
</DialogHeader>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-pulse">Loading photos...</div>
</div>
) : photos.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No photos to manage
</div>
) : (
<div className="space-y-4">
{photos.map((photo, index) => (
<Card key={photo.id}>
<CardContent className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<div className="w-full aspect-video sm:w-32 sm:h-32 flex-shrink-0 overflow-hidden rounded-lg">
<img
src={photo.cloudflare_image_url}
alt={photo.caption || 'Photo'}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 space-y-2">
<div>
<p className="text-sm sm:text-base text-foreground">
{photo.caption || (
<span className="text-muted-foreground italic">No caption</span>
)}
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setEditingPhoto(photo)}
className="flex-1 sm:flex-initial"
>
<Pencil className="w-4 h-4 mr-2" />
Request Edit
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDeleteClick(photo)}
className="flex-1 sm:flex-initial"
>
<Trash2 className="w-4 h-4 mr-2" />
Request Delete
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Request Photo Deletion</AlertDialogTitle>
<AlertDialogDescription>
Please provide a reason for deleting this photo. This request will be reviewed by moderators.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-2">
<Label htmlFor="delete-reason">Reason for deletion</Label>
<Textarea
id="delete-reason"
value={deleteReason}
onChange={(e) => setDeleteReason(e.target.value)}
placeholder="Please explain why this photo should be deleted..."
rows={3}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => {
setDeleteDialogOpen(false);
setPhotoToDelete(null);
setDeleteReason('');
}}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={requestPhotoDelete}
disabled={!deleteReason.trim()}
>
Submit Request
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Dialog>
);
}