Refactor photo modification logic

This commit is contained in:
gpt-engineer-app[bot]
2025-10-02 17:46:45 +00:00
parent 6f579faa31
commit 2750d285cb
5 changed files with 224 additions and 29 deletions

View File

@@ -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) {

View File

@@ -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();
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'
});
if (itemError) throw itemError;
await fetchPhotos();
toast({
title: 'Success',
description: 'Photo deleted',
title: 'Delete request submitted',
description: 'Your photo deletion request has been submitted for moderation',
});
onUpdate?.();
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>

View File

@@ -4,7 +4,9 @@ export type EntityType =
| 'manufacturer'
| 'operator'
| 'designer'
| 'property_owner';
| 'property_owner'
| 'photo_edit'
| 'photo_delete';
export interface PhotoSubmission {
url: string;

View File

@@ -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}`);
}

View File

@@ -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);