mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:31:13 -05:00
Refactor photo modification logic
This commit is contained in:
@@ -35,6 +35,7 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
const [cardImageUrl, setCardImageUrl] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||
const [isPhotoOperation, setIsPhotoOperation] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubmissionItems();
|
||||
@@ -57,6 +58,15 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
setItemData(firstItem.item_data);
|
||||
setOriginalData(firstItem.original_data);
|
||||
|
||||
// Check for photo edit/delete operations
|
||||
if (firstItem.item_type === 'photo_edit' || firstItem.item_type === 'photo_delete') {
|
||||
setIsPhotoOperation(true);
|
||||
if (firstItem.item_type === 'photo_edit') {
|
||||
setChangedFields(['caption']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse changed fields
|
||||
const changed: string[] = [];
|
||||
const data = firstItem.item_data as any;
|
||||
@@ -121,6 +131,63 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
);
|
||||
}
|
||||
|
||||
// Handle photo edit/delete operations
|
||||
if (isPhotoOperation) {
|
||||
const isEdit = changedFields.includes('caption');
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline">
|
||||
Photo
|
||||
</Badge>
|
||||
<Badge variant={isEdit ? "secondary" : "destructive"}>
|
||||
{isEdit ? 'Edit' : 'Delete'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{itemData?.cloudflare_image_url && (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-2">
|
||||
<img
|
||||
src={itemData.cloudflare_image_url}
|
||||
alt="Photo to be modified"
|
||||
className="w-full h-32 object-cover rounded"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isEdit && (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">Old caption: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{originalData?.caption || <em>No caption</em>}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">New caption: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{itemData?.new_caption || <em>No caption</em>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEdit && itemData?.reason && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">Reason: </span>
|
||||
<span className="text-muted-foreground">{itemData.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground italic">
|
||||
Click "Review Items" for full details
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Build photos array for modal
|
||||
const photos = [];
|
||||
if (bannerImageUrl) {
|
||||
|
||||
@@ -77,55 +77,128 @@ export function PhotoManagementDialog({
|
||||
|
||||
|
||||
|
||||
const deletePhoto = async (photoId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this photo?')) return;
|
||||
const requestPhotoDelete = async (photoId: string, photo: Photo) => {
|
||||
const reason = prompt('Please provide a reason for deleting this photo:');
|
||||
if (!reason) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('photos').delete().eq('id', photoId);
|
||||
// Get current user
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('Not authenticated');
|
||||
|
||||
if (error) throw error;
|
||||
// Create content submission
|
||||
const { data: submission, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert([{
|
||||
user_id: user.id,
|
||||
submission_type: 'photo_delete',
|
||||
content: {
|
||||
photo_id: photoId,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
reason: reason
|
||||
}
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
await fetchPhotos();
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Photo deleted',
|
||||
if (submissionError) throw submissionError;
|
||||
|
||||
// Create submission item
|
||||
const { error: itemError } = await supabase
|
||||
.from('submission_items')
|
||||
.insert({
|
||||
submission_id: submission.id,
|
||||
item_type: 'photo_delete',
|
||||
item_data: {
|
||||
photo_id: photoId,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
cloudflare_image_url: photo.cloudflare_image_url,
|
||||
caption: photo.caption,
|
||||
reason: reason
|
||||
},
|
||||
status: 'pending'
|
||||
});
|
||||
onUpdate?.();
|
||||
|
||||
if (itemError) throw itemError;
|
||||
|
||||
toast({
|
||||
title: 'Delete request submitted',
|
||||
description: 'Your photo deletion request has been submitted for moderation',
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error('Error deleting photo:', error);
|
||||
console.error('Error requesting photo deletion:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to delete photo',
|
||||
description: 'Failed to submit deletion request',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updatePhoto = async () => {
|
||||
const requestPhotoEdit = async () => {
|
||||
if (!editingPhoto) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('photos')
|
||||
.update({
|
||||
caption: editingPhoto.caption,
|
||||
})
|
||||
.eq('id', editingPhoto.id);
|
||||
// Get current user
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('Not authenticated');
|
||||
|
||||
if (error) throw error;
|
||||
// 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: {
|
||||
photo_id: editingPhoto.id,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId
|
||||
}
|
||||
}])
|
||||
.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;
|
||||
|
||||
await fetchPhotos();
|
||||
setEditingPhoto(null);
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Photo updated',
|
||||
title: 'Edit request submitted',
|
||||
description: 'Your photo edit has been submitted for moderation',
|
||||
});
|
||||
onUpdate?.();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error('Error updating photo:', error);
|
||||
console.error('Error requesting photo edit:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update photo',
|
||||
description: 'Failed to submit edit request',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -167,7 +240,7 @@ export function PhotoManagementDialog({
|
||||
<Button variant="outline" onClick={() => setEditingPhoto(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={updatePhoto}>Save Changes</Button>
|
||||
<Button onClick={requestPhotoEdit}>Submit for Review</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -223,16 +296,16 @@ export function PhotoManagementDialog({
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-2" />
|
||||
Edit
|
||||
Request Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => deletePhoto(photo.id)}
|
||||
onClick={() => requestPhotoDelete(photo.id, photo)}
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
Request Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,9 @@ export type EntityType =
|
||||
| 'manufacturer'
|
||||
| 'operator'
|
||||
| 'designer'
|
||||
| 'property_owner';
|
||||
| 'property_owner'
|
||||
| 'photo_edit'
|
||||
| 'photo_delete';
|
||||
|
||||
export interface PhotoSubmission {
|
||||
url: string;
|
||||
|
||||
@@ -120,6 +120,14 @@ serve(async (req) => {
|
||||
await approvePhotos(supabase, resolvedData, item.id);
|
||||
entityId = item.id; // Use item ID as entity ID for photos
|
||||
break;
|
||||
case 'photo_edit':
|
||||
await editPhoto(supabase, resolvedData);
|
||||
entityId = resolvedData.photo_id;
|
||||
break;
|
||||
case 'photo_delete':
|
||||
await deletePhoto(supabase, resolvedData);
|
||||
entityId = resolvedData.photo_id;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown item type: ${item.item_type}`);
|
||||
}
|
||||
@@ -595,3 +603,25 @@ function extractImageId(url: string): string {
|
||||
const matches = url.match(/\/([^\/]+)\/public$/);
|
||||
return matches ? matches[1] : url;
|
||||
}
|
||||
|
||||
async function editPhoto(supabase: any, data: any): Promise<void> {
|
||||
console.log(`Editing photo ${data.photo_id}`);
|
||||
const { error } = await supabase
|
||||
.from('photos')
|
||||
.update({
|
||||
caption: data.new_caption,
|
||||
})
|
||||
.eq('id', data.photo_id);
|
||||
|
||||
if (error) throw new Error(`Failed to edit photo: ${error.message}`);
|
||||
}
|
||||
|
||||
async function deletePhoto(supabase: any, data: any): Promise<void> {
|
||||
console.log(`Deleting photo ${data.photo_id}`);
|
||||
const { error } = await supabase
|
||||
.from('photos')
|
||||
.delete()
|
||||
.eq('id', data.photo_id);
|
||||
|
||||
if (error) throw new Error(`Failed to delete photo: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Restrict direct photo modifications - require moderation queue
|
||||
-- Drop existing policies that allow direct modification
|
||||
DROP POLICY IF EXISTS "Moderators can update photos" ON public.photos;
|
||||
DROP POLICY IF EXISTS "Moderators can delete photos" ON public.photos;
|
||||
|
||||
-- Keep read policies
|
||||
-- Public read access to photos already exists
|
||||
|
||||
-- Only service role (edge functions) can modify photos after approval
|
||||
CREATE POLICY "Service role can insert photos"
|
||||
ON public.photos FOR INSERT
|
||||
TO service_role
|
||||
WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Service role can update photos"
|
||||
ON public.photos FOR UPDATE
|
||||
TO service_role
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Service role can delete photos"
|
||||
ON public.photos FOR DELETE
|
||||
TO service_role
|
||||
USING (true);
|
||||
Reference in New Issue
Block a user