From 0ddae7493cf90820f1e184a8c508adcaf6df480b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:45:35 +0000 Subject: [PATCH] Implement Park Form and Edit/Add Functionality --- src/components/admin/ParkForm.tsx | 68 ++++++--- src/integrations/supabase/types.ts | 6 + src/pages/ParkDetail.tsx | 135 +++++++++++++++++- src/pages/Parks.tsx | 125 ++++++++++++++-- src/types/database.ts | 2 + ...9_cdbc4c2d-8e86-4cd9-8dc3-7f69a3da5d7e.sql | 4 + 6 files changed, 311 insertions(+), 29 deletions(-) create mode 100644 supabase/migrations/20250929234319_cdbc4c2d-8e86-4cd9-8dc3-7f69a3da5d7e.sql diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 9e4ee75e..ac68c527 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; -import { PhotoUpload } from '@/components/upload/PhotoUpload'; +import { UppyPhotoUpload } from '@/components/upload/UppyPhotoUpload'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DatePicker } from '@/components/ui/date-picker'; import { toast } from '@/hooks/use-toast'; @@ -29,9 +29,9 @@ const parkSchema = z.object({ type ParkFormData = z.infer; interface ParkFormProps { - onSubmit: (data: ParkFormData & { banner_image_url?: string; card_image_url?: string }) => Promise; + onSubmit: (data: ParkFormData & { banner_image_url?: string; card_image_url?: string; banner_image_id?: string; card_image_id?: string }) => Promise; onCancel?: () => void; - initialData?: Partial; + initialData?: Partial; isEditing?: boolean; } @@ -59,6 +59,14 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: const [submitting, setSubmitting] = useState(false); const [bannerImage, setBannerImage] = useState(initialData?.banner_image_url || ''); const [cardImage, setCardImage] = useState(initialData?.card_image_url || ''); + const [bannerImageId, setBannerImageId] = useState(initialData?.banner_image_id || ''); + const [cardImageId, setCardImageId] = useState(initialData?.card_image_id || ''); + + // Extract Cloudflare image ID from URL + const extractImageId = (url: string): string => { + const match = url.match(/\/([a-f0-9-]{36})\//); + return match ? match[1] : ''; + }; const { register, @@ -103,7 +111,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: await onSubmit({ ...data, banner_image_url: bannerImage || undefined, - card_image_url: cardImage || undefined + card_image_url: cardImage || undefined, + banner_image_id: bannerImageId || undefined, + card_image_id: cardImageId || undefined }); toast({ @@ -283,15 +293,28 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
- + Current banner +
+ )} + setBannerImage(urls[0] || '')} - onError={(error) => { + variant="public" + onUploadComplete={(urls) => { + const url = urls[0]; + if (url) { + setBannerImage(url); + const imageId = extractImageId(url); + if (imageId) { + setBannerImageId(imageId); + } + } + }} + onUploadError={(error) => { toast({ title: "Upload Error", - description: error, + description: error.message, variant: "destructive" }); }} @@ -303,15 +326,28 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
- + Current card +
+ )} + setCardImage(urls[0] || '')} - onError={(error) => { + variant="public" + onUploadComplete={(urls) => { + const url = urls[0]; + if (url) { + setCardImage(url); + const imageId = extractImageId(url); + if (imageId) { + setCardImageId(imageId); + } + } + }} + onUploadError={(error) => { toast({ title: "Upload Error", - description: error, + description: error.message, variant: "destructive" }); }} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 09342141..dbd69c68 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -250,7 +250,9 @@ export type Database = { parks: { Row: { average_rating: number | null + banner_image_id: string | null banner_image_url: string | null + card_image_id: string | null card_image_url: string | null closing_date: string | null coaster_count: number | null @@ -274,7 +276,9 @@ export type Database = { } Insert: { average_rating?: number | null + banner_image_id?: string | null banner_image_url?: string | null + card_image_id?: string | null card_image_url?: string | null closing_date?: string | null coaster_count?: number | null @@ -298,7 +302,9 @@ export type Database = { } Update: { average_rating?: number | null + banner_image_id?: string | null banner_image_url?: string | null + card_image_id?: string | null card_image_url?: string | null closing_date?: string | null coaster_count?: number | null diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx index 282bcef1..0d1d8be0 100644 --- a/src/pages/ParkDetail.tsx +++ b/src/pages/ParkDetail.tsx @@ -16,7 +16,10 @@ import { EntityPhotoGallery } from '@/components/upload/EntityPhotoGallery'; import { supabase } from '@/integrations/supabase/client'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { RideForm } from '@/components/admin/RideForm'; +import { ParkForm } from '@/components/admin/ParkForm'; import { toast } from '@/hooks/use-toast'; +import { useUserRole } from '@/hooks/useUserRole'; +import { Edit } from 'lucide-react'; export default function ParkDetail() { const { slug @@ -29,6 +32,8 @@ export default function ParkDetail() { const [rides, setRides] = useState([]); const [loading, setLoading] = useState(true); const [isAddRideModalOpen, setIsAddRideModalOpen] = useState(false); + const [isEditParkModalOpen, setIsEditParkModalOpen] = useState(false); + const { isModerator } = useUserRole(); useEffect(() => { if (slug) { fetchParkData(); @@ -153,10 +158,87 @@ export default function ParkDetail() { toast({ title: "Submission Failed", description: error.message || "Failed to submit ride for review.", + variant: "destructive" + }); + } + }; + + const handleEditParkClick = () => { + if (!user) { + navigate('/auth'); + return; + } + setIsEditParkModalOpen(true); + }; + + const handleEditParkSubmit = async (parkData: any) => { + if (!user || !park) return; + + try { + if (isModerator()) { + // Moderators can update directly + const { error } = await supabase + .from('parks') + .update({ + name: parkData.name, + slug: parkData.slug, + description: parkData.description, + park_type: parkData.park_type, + status: parkData.status, + opening_date: parkData.opening_date || null, + closing_date: parkData.closing_date || null, + website_url: parkData.website_url || null, + phone: parkData.phone || null, + email: parkData.email || null, + banner_image_url: parkData.banner_image_url || null, + banner_image_id: parkData.banner_image_id || null, + card_image_url: parkData.card_image_url || null, + card_image_id: parkData.card_image_id || null, + updated_at: new Date().toISOString() + }) + .eq('id', park.id); + + if (error) throw error; + + toast({ + title: "Park Updated", + description: "The park has been updated successfully.", + }); + + setIsEditParkModalOpen(false); + fetchParkData(); + } else { + // Regular users submit for moderation + const { error } = await supabase + .from('content_submissions') + .insert({ + user_id: user.id, + submission_type: 'park_edit', + status: 'pending', + content: { + park_id: park.id, + ...parkData + } + }); + + if (error) throw error; + + toast({ + title: "Edit Submitted", + description: "Your park edit has been submitted for review.", + }); + + setIsEditParkModalOpen(false); + } + } catch (error: any) { + toast({ + title: "Error", + description: error.message || "Failed to update park.", variant: "destructive" }); } }; + if (loading) { return
@@ -190,11 +272,21 @@ export default function ParkDetail() {
- {/* Back Button */} - + {/* Back Button and Edit Button */} +
+ + + +
{/* Hero Section */}
@@ -544,6 +636,39 @@ export default function ParkDetail() { /> + + {/* Edit Park Modal */} + + + + Edit Park + + Make changes to the park information. {isModerator() ? 'Changes will be applied immediately.' : 'Your changes will be submitted for review.'} + + + setIsEditParkModalOpen(false)} + initialData={{ + name: park?.name, + slug: park?.slug, + description: park?.description, + park_type: park?.park_type, + status: park?.status, + opening_date: park?.opening_date, + closing_date: park?.closing_date, + website_url: park?.website_url, + phone: park?.phone, + email: park?.email, + banner_image_url: park?.banner_image_url, + banner_image_id: park?.banner_image_id, + card_image_url: park?.card_image_url, + card_image_id: park?.card_image_id + }} + isEditing={true} + /> + +
; } \ No newline at end of file diff --git a/src/pages/Parks.tsx b/src/pages/Parks.tsx index 9adc7c96..dffb33ce 100644 --- a/src/pages/Parks.tsx +++ b/src/pages/Parks.tsx @@ -15,7 +15,8 @@ import { ChevronDown, Sliders, X, - FerrisWheel + FerrisWheel, + Plus } from 'lucide-react'; import { Park } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; @@ -26,6 +27,10 @@ import { ParkListView } from '@/components/parks/ParkListView'; import { ParkSearch } from '@/components/parks/ParkSearch'; import { ParkSortOptions } from '@/components/parks/ParkSortOptions'; import { useToast } from '@/hooks/use-toast'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { ParkForm } from '@/components/admin/ParkForm'; +import { useAuth } from '@/hooks/useAuth'; +import { useUserRole } from '@/hooks/useUserRole'; export interface FilterState { search: string; @@ -70,8 +75,11 @@ export default function Parks() { const [sort, setSort] = useState(initialSort); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [showFilters, setShowFilters] = useState(false); + const [isAddParkModalOpen, setIsAddParkModalOpen] = useState(false); const navigate = useNavigate(); const { toast } = useToast(); + const { user } = useAuth(); + const { isModerator } = useUserRole(); useEffect(() => { fetchParks(); @@ -227,6 +235,80 @@ export default function Parks() { navigate(`/parks/${park.slug}`); }; + const handleAddParkClick = () => { + if (!user) { + navigate('/auth'); + return; + } + setIsAddParkModalOpen(true); + }; + + const handleParkSubmit = async (parkData: any) => { + if (!user) { + navigate('/auth'); + return; + } + + try { + if (isModerator()) { + // Moderators can create parks directly + const { error } = await supabase + .from('parks') + .insert({ + name: parkData.name, + slug: parkData.slug, + description: parkData.description || null, + park_type: parkData.park_type, + status: parkData.status, + opening_date: parkData.opening_date || null, + closing_date: parkData.closing_date || null, + website_url: parkData.website_url || null, + phone: parkData.phone || null, + email: parkData.email || null, + banner_image_url: parkData.banner_image_url || null, + banner_image_id: parkData.banner_image_id || null, + card_image_url: parkData.card_image_url || null, + card_image_id: parkData.card_image_id || null + }); + + if (error) throw error; + + toast({ + title: "Park Created", + description: "The park has been created successfully.", + }); + + setIsAddParkModalOpen(false); + fetchParks(); + } else { + // Regular users submit for moderation + const { error } = await supabase + .from('content_submissions') + .insert({ + user_id: user.id, + submission_type: 'park', + status: 'pending', + content: parkData + }); + + if (error) throw error; + + toast({ + title: "Submission Sent", + description: "Your park submission has been sent for moderation review.", + }); + + setIsAddParkModalOpen(false); + } + } catch (error: any) { + toast({ + title: "Submission Failed", + description: error.message || "Failed to submit park.", + variant: "destructive" + }); + } + }; + if (loading) { return (
@@ -278,16 +360,26 @@ export default function Parks() {
- {activeFilterCount > 0 && ( +
- )} + + {activeFilterCount > 0 && ( + + )} +
@@ -374,6 +466,23 @@ export default function Parks() { )} + + {/* Add Park Modal */} + + + + Add New Park + + Add a new park to the database. {isModerator() ? 'The park will be added immediately.' : 'Your submission will be reviewed before being published.'} + + + setIsAddParkModalOpen(false)} + isEditing={false} + /> + + ); diff --git a/src/types/database.ts b/src/types/database.ts index 299e16fa..bff5819a 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -41,7 +41,9 @@ export interface Park { operator?: Company; property_owner?: Company; banner_image_url?: string; + banner_image_id?: string; card_image_url?: string; + card_image_id?: string; average_rating: number; review_count: number; ride_count: number; diff --git a/supabase/migrations/20250929234319_cdbc4c2d-8e86-4cd9-8dc3-7f69a3da5d7e.sql b/supabase/migrations/20250929234319_cdbc4c2d-8e86-4cd9-8dc3-7f69a3da5d7e.sql new file mode 100644 index 00000000..79090a11 --- /dev/null +++ b/supabase/migrations/20250929234319_cdbc4c2d-8e86-4cd9-8dc3-7f69a3da5d7e.sql @@ -0,0 +1,4 @@ +-- Add Cloudflare image ID columns to parks table +ALTER TABLE public.parks +ADD COLUMN IF NOT EXISTS banner_image_id TEXT, +ADD COLUMN IF NOT EXISTS card_image_id TEXT; \ No newline at end of file