From 6f579faa312ad972d7d8b94f80bf2b1b9878dc67 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:39:27 +0000 Subject: [PATCH 01/71] Fix reviews query and NaN handling --- src/components/reviews/ReviewForm.tsx | 3 ++- src/components/reviews/ReviewsList.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/reviews/ReviewForm.tsx b/src/components/reviews/ReviewForm.tsx index 70107e84..2f9ae68b 100644 --- a/src/components/reviews/ReviewForm.tsx +++ b/src/components/reviews/ReviewForm.tsx @@ -189,7 +189,8 @@ export function ReviewForm({ {entityType === 'ride' &&
v === '' || isNaN(v) ? undefined : v })} />
} diff --git a/src/components/reviews/ReviewsList.tsx b/src/components/reviews/ReviewsList.tsx index 6d2441e3..38a3eca7 100644 --- a/src/components/reviews/ReviewsList.tsx +++ b/src/components/reviews/ReviewsList.tsx @@ -49,7 +49,7 @@ export function ReviewsList({ entityType, entityId, entityName }: ReviewsListPro .from('reviews') .select(` *, - profiles:user_id(username, avatar_url, display_name) + profiles!reviews_user_id_fkey(username, avatar_url, display_name) `) .eq('moderation_status', 'approved') .order('created_at', { ascending: false }); From 2750d285cbd2f05aea8b782a31dfd2558ace2d98 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:46:45 +0000 Subject: [PATCH 02/71] Refactor photo modification logic --- .../moderation/EntityEditPreview.tsx | 67 +++++++++ .../upload/PhotoManagementDialog.tsx | 129 ++++++++++++++---- src/types/submissions.ts | 4 +- .../process-selective-approval/index.ts | 30 ++++ ...4_ddacac58-98d8-454f-9e1b-658b9ac6b734.sql | 23 ++++ 5 files changed, 224 insertions(+), 29 deletions(-) create mode 100644 supabase/migrations/20251002174244_ddacac58-98d8-454f-9e1b-658b9ac6b734.sql diff --git a/src/components/moderation/EntityEditPreview.tsx b/src/components/moderation/EntityEditPreview.tsx index 9b436461..6a522501 100644 --- a/src/components/moderation/EntityEditPreview.tsx +++ b/src/components/moderation/EntityEditPreview.tsx @@ -35,6 +35,7 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti const [cardImageUrl, setCardImageUrl] = useState(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 ( +
+
+ + Photo + + + {isEdit ? 'Edit' : 'Delete'} + +
+ + {itemData?.cloudflare_image_url && ( + + + Photo to be modified + + + )} + + {isEdit && ( +
+
+ Old caption: + + {originalData?.caption || No caption} + +
+
+ New caption: + + {itemData?.new_caption || No caption} + +
+
+ )} + + {!isEdit && itemData?.reason && ( +
+ Reason: + {itemData.reason} +
+ )} + +
+ Click "Review Items" for full details +
+
+ ); + } + // Build photos array for modal const photos = []; if (bannerImageUrl) { diff --git a/src/components/upload/PhotoManagementDialog.tsx b/src/components/upload/PhotoManagementDialog.tsx index ee90b0dd..ef96cc00 100644 --- a/src/components/upload/PhotoManagementDialog.tsx +++ b/src/components/upload/PhotoManagementDialog.tsx @@ -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({ - + @@ -223,16 +296,16 @@ export function PhotoManagementDialog({ className="flex-1 sm:flex-initial" > - Edit + Request Edit diff --git a/src/types/submissions.ts b/src/types/submissions.ts index 64f0b83c..52d36309 100644 --- a/src/types/submissions.ts +++ b/src/types/submissions.ts @@ -4,7 +4,9 @@ export type EntityType = | 'manufacturer' | 'operator' | 'designer' - | 'property_owner'; + | 'property_owner' + | 'photo_edit' + | 'photo_delete'; export interface PhotoSubmission { url: string; diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 28ad833d..9bddaf82 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -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 { + 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 { + 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}`); +} diff --git a/supabase/migrations/20251002174244_ddacac58-98d8-454f-9e1b-658b9ac6b734.sql b/supabase/migrations/20251002174244_ddacac58-98d8-454f-9e1b-658b9ac6b734.sql new file mode 100644 index 00000000..075c0ec8 --- /dev/null +++ b/supabase/migrations/20251002174244_ddacac58-98d8-454f-9e1b-658b9ac6b734.sql @@ -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); \ No newline at end of file From 7c7e24374139797382202c86b526c44d6837fc5b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:49:28 +0000 Subject: [PATCH 03/71] Fix wait_time_minutes NaN validation --- src/components/reviews/ReviewForm.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/reviews/ReviewForm.tsx b/src/components/reviews/ReviewForm.tsx index 2f9ae68b..c89592e5 100644 --- a/src/components/reviews/ReviewForm.tsx +++ b/src/components/reviews/ReviewForm.tsx @@ -19,7 +19,14 @@ const reviewSchema = z.object({ title: z.string().optional(), content: z.string().min(10, 'Review must be at least 10 characters long'), visit_date: z.string().optional(), - wait_time_minutes: z.number().optional(), + wait_time_minutes: z.preprocess( + (val) => { + if (val === '' || val === null || val === undefined) return undefined; + const num = Number(val); + return isNaN(num) ? undefined : num; + }, + z.number().positive().optional() + ), photos: z.array(z.string()).optional() }); type ReviewFormData = z.infer; From a11e524fa31aef2d5898ba1647d73babebf79806 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:59:02 +0000 Subject: [PATCH 04/71] Add foreign key to reviews table --- src/components/reviews/ReviewsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/reviews/ReviewsList.tsx b/src/components/reviews/ReviewsList.tsx index 38a3eca7..d747b2b6 100644 --- a/src/components/reviews/ReviewsList.tsx +++ b/src/components/reviews/ReviewsList.tsx @@ -49,7 +49,7 @@ export function ReviewsList({ entityType, entityId, entityName }: ReviewsListPro .from('reviews') .select(` *, - profiles!reviews_user_id_fkey(username, avatar_url, display_name) + profiles!user_id(username, avatar_url, display_name) `) .eq('moderation_status', 'approved') .order('created_at', { ascending: false }); From c0637fabd0dc1dc0a805f3a2bf3dfc387c90f90d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:24:08 +0000 Subject: [PATCH 05/71] Fix reviews query foreign key --- src/components/reviews/ReviewsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/reviews/ReviewsList.tsx b/src/components/reviews/ReviewsList.tsx index d747b2b6..38a3eca7 100644 --- a/src/components/reviews/ReviewsList.tsx +++ b/src/components/reviews/ReviewsList.tsx @@ -49,7 +49,7 @@ export function ReviewsList({ entityType, entityId, entityName }: ReviewsListPro .from('reviews') .select(` *, - profiles!user_id(username, avatar_url, display_name) + profiles!reviews_user_id_fkey(username, avatar_url, display_name) `) .eq('moderation_status', 'approved') .order('created_at', { ascending: false }); From 8fe5bf5c1c134ef1bf96df41ce402f954edf918e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:33:31 +0000 Subject: [PATCH 06/71] Fix: Correct foreign key constraint for reviews --- src/integrations/supabase/types.ts | 7 +++++++ ...1002183319_02a2dbf8-9529-4479-af0c-1f3d93f447fd.sql | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 supabase/migrations/20251002183319_02a2dbf8-9529-4479-af0c-1f3d93f447fd.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 835dc856..e99ca3ed 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1101,6 +1101,13 @@ export type Database = { referencedRelation: "rides" referencedColumns: ["id"] }, + { + foreignKeyName: "reviews_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["user_id"] + }, ] } ride_coaster_statistics: { diff --git a/supabase/migrations/20251002183319_02a2dbf8-9529-4479-af0c-1f3d93f447fd.sql b/supabase/migrations/20251002183319_02a2dbf8-9529-4479-af0c-1f3d93f447fd.sql new file mode 100644 index 00000000..2b1cc404 --- /dev/null +++ b/supabase/migrations/20251002183319_02a2dbf8-9529-4479-af0c-1f3d93f447fd.sql @@ -0,0 +1,10 @@ +-- Drop existing foreign key that points to auth.users +ALTER TABLE public.reviews +DROP CONSTRAINT IF EXISTS reviews_user_id_fkey; + +-- Add new foreign key constraint pointing to profiles.user_id +ALTER TABLE public.reviews +ADD CONSTRAINT reviews_user_id_fkey +FOREIGN KEY (user_id) +REFERENCES public.profiles(user_id) +ON DELETE CASCADE; \ No newline at end of file From 5794b5bf3d214a46d7669fc93ac5a8d3692a7e5e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:37:23 +0000 Subject: [PATCH 07/71] Fix: Remove star rating row --- src/pages/RideDetail.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx index 7265f67a..0f570526 100644 --- a/src/pages/RideDetail.tsx +++ b/src/pages/RideDetail.tsx @@ -261,18 +261,6 @@ export default function RideDetail() { {ride.average_rating.toFixed(1)} -
- {[1, 2, 3, 4, 5].map((star) => ( - - ))} -
{ride.review_count} {ride.review_count === 1 ? "review" : "reviews"}
From 93750bdb042e69e55ee9bd279318848cff65fc62 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:03:04 +0000 Subject: [PATCH 08/71] Refactor: Use Radix UI Dialog --- .../upload/PhotoManagementDialog.tsx | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/src/components/upload/PhotoManagementDialog.tsx b/src/components/upload/PhotoManagementDialog.tsx index ef96cc00..09871c18 100644 --- a/src/components/upload/PhotoManagementDialog.tsx +++ b/src/components/upload/PhotoManagementDialog.tsx @@ -9,6 +9,16 @@ import { 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'; @@ -43,6 +53,9 @@ export function PhotoManagementDialog({ const [photos, setPhotos] = useState([]); const [loading, setLoading] = useState(false); const [editingPhoto, setEditingPhoto] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [photoToDelete, setPhotoToDelete] = useState(null); + const [deleteReason, setDeleteReason] = useState(''); const { toast } = useToast(); useEffect(() => { @@ -77,9 +90,14 @@ export function PhotoManagementDialog({ - const requestPhotoDelete = async (photoId: string, photo: Photo) => { - const reason = prompt('Please provide a reason for deleting this photo:'); - if (!reason) return; + const handleDeleteClick = (photo: Photo) => { + setPhotoToDelete(photo); + setDeleteReason(''); + setDeleteDialogOpen(true); + }; + + const requestPhotoDelete = async () => { + if (!photoToDelete || !deleteReason.trim()) return; try { // Get current user @@ -93,10 +111,10 @@ export function PhotoManagementDialog({ user_id: user.id, submission_type: 'photo_delete', content: { - photo_id: photoId, + photo_id: photoToDelete.id, entity_type: entityType, entity_id: entityId, - reason: reason + reason: deleteReason } }]) .select() @@ -111,12 +129,12 @@ export function PhotoManagementDialog({ submission_id: submission.id, item_type: 'photo_delete', item_data: { - photo_id: photoId, + photo_id: photoToDelete.id, entity_type: entityType, entity_id: entityId, - cloudflare_image_url: photo.cloudflare_image_url, - caption: photo.caption, - reason: reason + cloudflare_image_url: photoToDelete.cloudflare_image_url, + caption: photoToDelete.caption, + reason: deleteReason }, status: 'pending' }); @@ -127,6 +145,9 @@ export function PhotoManagementDialog({ title: 'Delete request submitted', description: 'Your photo deletion request has been submitted for moderation', }); + setDeleteDialogOpen(false); + setPhotoToDelete(null); + setDeleteReason(''); onOpenChange(false); } catch (error) { console.error('Error requesting photo deletion:', error); @@ -301,7 +322,7 @@ export function PhotoManagementDialog({ + + + + + Request Photo Deletion + + Please provide a reason for deleting this photo. This request will be reviewed by moderators. + + + +
+ +