mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:31:11 -05:00
Implement Park Form and Edit/Add Functionality
This commit is contained in:
@@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { DatePicker } from '@/components/ui/date-picker';
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
@@ -29,9 +29,9 @@ const parkSchema = z.object({
|
|||||||
type ParkFormData = z.infer<typeof parkSchema>;
|
type ParkFormData = z.infer<typeof parkSchema>;
|
||||||
|
|
||||||
interface ParkFormProps {
|
interface ParkFormProps {
|
||||||
onSubmit: (data: ParkFormData & { banner_image_url?: string; card_image_url?: string }) => Promise<void>;
|
onSubmit: (data: ParkFormData & { banner_image_url?: string; card_image_url?: string; banner_image_id?: string; card_image_id?: string }) => Promise<void>;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
initialData?: Partial<ParkFormData & { banner_image_url?: string; card_image_url?: string }>;
|
initialData?: Partial<ParkFormData & { banner_image_url?: string; card_image_url?: string; banner_image_id?: string; card_image_id?: string }>;
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +59,14 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [bannerImage, setBannerImage] = useState<string>(initialData?.banner_image_url || '');
|
const [bannerImage, setBannerImage] = useState<string>(initialData?.banner_image_url || '');
|
||||||
const [cardImage, setCardImage] = useState<string>(initialData?.card_image_url || '');
|
const [cardImage, setCardImage] = useState<string>(initialData?.card_image_url || '');
|
||||||
|
const [bannerImageId, setBannerImageId] = useState<string>(initialData?.banner_image_id || '');
|
||||||
|
const [cardImageId, setCardImageId] = useState<string>(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 {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -103,7 +111,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
await onSubmit({
|
await onSubmit({
|
||||||
...data,
|
...data,
|
||||||
banner_image_url: bannerImage || undefined,
|
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({
|
toast({
|
||||||
@@ -283,15 +293,28 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Banner Image</Label>
|
<Label>Banner Image</Label>
|
||||||
<PhotoUpload
|
{bannerImage && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<img src={bannerImage} alt="Current banner" className="h-32 w-full object-cover rounded-md" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<UppyPhotoUpload
|
||||||
maxFiles={1}
|
maxFiles={1}
|
||||||
variant="default"
|
variant="public"
|
||||||
existingPhotos={bannerImage ? [bannerImage] : []}
|
onUploadComplete={(urls) => {
|
||||||
onUploadComplete={(urls) => setBannerImage(urls[0] || '')}
|
const url = urls[0];
|
||||||
onError={(error) => {
|
if (url) {
|
||||||
|
setBannerImage(url);
|
||||||
|
const imageId = extractImageId(url);
|
||||||
|
if (imageId) {
|
||||||
|
setBannerImageId(imageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onUploadError={(error) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Upload Error",
|
title: "Upload Error",
|
||||||
description: error,
|
description: error.message,
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -303,15 +326,28 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Card Image</Label>
|
<Label>Card Image</Label>
|
||||||
<PhotoUpload
|
{cardImage && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<img src={cardImage} alt="Current card" className="h-32 w-full object-cover rounded-md" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<UppyPhotoUpload
|
||||||
maxFiles={1}
|
maxFiles={1}
|
||||||
variant="default"
|
variant="public"
|
||||||
existingPhotos={cardImage ? [cardImage] : []}
|
onUploadComplete={(urls) => {
|
||||||
onUploadComplete={(urls) => setCardImage(urls[0] || '')}
|
const url = urls[0];
|
||||||
onError={(error) => {
|
if (url) {
|
||||||
|
setCardImage(url);
|
||||||
|
const imageId = extractImageId(url);
|
||||||
|
if (imageId) {
|
||||||
|
setCardImageId(imageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onUploadError={(error) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Upload Error",
|
title: "Upload Error",
|
||||||
description: error,
|
description: error.message,
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -250,7 +250,9 @@ export type Database = {
|
|||||||
parks: {
|
parks: {
|
||||||
Row: {
|
Row: {
|
||||||
average_rating: number | null
|
average_rating: number | null
|
||||||
|
banner_image_id: string | null
|
||||||
banner_image_url: string | null
|
banner_image_url: string | null
|
||||||
|
card_image_id: string | null
|
||||||
card_image_url: string | null
|
card_image_url: string | null
|
||||||
closing_date: string | null
|
closing_date: string | null
|
||||||
coaster_count: number | null
|
coaster_count: number | null
|
||||||
@@ -274,7 +276,9 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
average_rating?: number | null
|
average_rating?: number | null
|
||||||
|
banner_image_id?: string | null
|
||||||
banner_image_url?: string | null
|
banner_image_url?: string | null
|
||||||
|
card_image_id?: string | null
|
||||||
card_image_url?: string | null
|
card_image_url?: string | null
|
||||||
closing_date?: string | null
|
closing_date?: string | null
|
||||||
coaster_count?: number | null
|
coaster_count?: number | null
|
||||||
@@ -298,7 +302,9 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
average_rating?: number | null
|
average_rating?: number | null
|
||||||
|
banner_image_id?: string | null
|
||||||
banner_image_url?: string | null
|
banner_image_url?: string | null
|
||||||
|
card_image_id?: string | null
|
||||||
card_image_url?: string | null
|
card_image_url?: string | null
|
||||||
closing_date?: string | null
|
closing_date?: string | null
|
||||||
coaster_count?: number | null
|
coaster_count?: number | null
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import { EntityPhotoGallery } from '@/components/upload/EntityPhotoGallery';
|
|||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { RideForm } from '@/components/admin/RideForm';
|
import { RideForm } from '@/components/admin/RideForm';
|
||||||
|
import { ParkForm } from '@/components/admin/ParkForm';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
|
import { Edit } from 'lucide-react';
|
||||||
export default function ParkDetail() {
|
export default function ParkDetail() {
|
||||||
const {
|
const {
|
||||||
slug
|
slug
|
||||||
@@ -29,6 +32,8 @@ export default function ParkDetail() {
|
|||||||
const [rides, setRides] = useState<Ride[]>([]);
|
const [rides, setRides] = useState<Ride[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isAddRideModalOpen, setIsAddRideModalOpen] = useState(false);
|
const [isAddRideModalOpen, setIsAddRideModalOpen] = useState(false);
|
||||||
|
const [isEditParkModalOpen, setIsEditParkModalOpen] = useState(false);
|
||||||
|
const { isModerator } = useUserRole();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchParkData();
|
fetchParkData();
|
||||||
@@ -153,10 +158,87 @@ export default function ParkDetail() {
|
|||||||
toast({
|
toast({
|
||||||
title: "Submission Failed",
|
title: "Submission Failed",
|
||||||
description: error.message || "Failed to submit ride for review.",
|
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"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="min-h-screen bg-background">
|
return <div className="min-h-screen bg-background">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -190,11 +272,21 @@ export default function ParkDetail() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
{/* Back Button */}
|
{/* Back Button and Edit Button */}
|
||||||
<Button variant="ghost" onClick={() => navigate('/parks')} className="mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
<Button variant="ghost" onClick={() => navigate('/parks')}>
|
||||||
Back to Parks
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
Back to Parks
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleEditParkClick}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
Edit Park
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="relative mb-8">
|
<div className="relative mb-8">
|
||||||
@@ -544,6 +636,39 @@ export default function ParkDetail() {
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Edit Park Modal */}
|
||||||
|
<Dialog open={isEditParkModalOpen} onOpenChange={setIsEditParkModalOpen}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Park</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Make changes to the park information. {isModerator() ? 'Changes will be applied immediately.' : 'Your changes will be submitted for review.'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<ParkForm
|
||||||
|
onSubmit={handleEditParkSubmit}
|
||||||
|
onCancel={() => 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}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</main>
|
</main>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Sliders,
|
Sliders,
|
||||||
X,
|
X,
|
||||||
FerrisWheel
|
FerrisWheel,
|
||||||
|
Plus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Park } from '@/types/database';
|
import { Park } from '@/types/database';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
@@ -26,6 +27,10 @@ import { ParkListView } from '@/components/parks/ParkListView';
|
|||||||
import { ParkSearch } from '@/components/parks/ParkSearch';
|
import { ParkSearch } from '@/components/parks/ParkSearch';
|
||||||
import { ParkSortOptions } from '@/components/parks/ParkSortOptions';
|
import { ParkSortOptions } from '@/components/parks/ParkSortOptions';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
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 {
|
export interface FilterState {
|
||||||
search: string;
|
search: string;
|
||||||
@@ -70,8 +75,11 @@ export default function Parks() {
|
|||||||
const [sort, setSort] = useState<SortState>(initialSort);
|
const [sort, setSort] = useState<SortState>(initialSort);
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [isAddParkModalOpen, setIsAddParkModalOpen] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { isModerator } = useUserRole();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchParks();
|
fetchParks();
|
||||||
@@ -227,6 +235,80 @@ export default function Parks() {
|
|||||||
navigate(`/parks/${park.slug}`);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
@@ -278,16 +360,26 @@ export default function Parks() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeFilterCount > 0 && (
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
onClick={handleAddParkClick}
|
||||||
onClick={clearAllFilters}
|
className="gap-2"
|
||||||
className="text-destructive hover:text-destructive"
|
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4" />
|
||||||
Clear all filters
|
Add Park
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
|
{activeFilterCount > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 mr-2" />
|
||||||
|
Clear all filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -374,6 +466,23 @@ export default function Parks() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add Park Modal */}
|
||||||
|
<Dialog open={isAddParkModalOpen} onOpenChange={setIsAddParkModalOpen}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Park</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Add a new park to the database. {isModerator() ? 'The park will be added immediately.' : 'Your submission will be reviewed before being published.'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<ParkForm
|
||||||
|
onSubmit={handleParkSubmit}
|
||||||
|
onCancel={() => setIsAddParkModalOpen(false)}
|
||||||
|
isEditing={false}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export interface Park {
|
|||||||
operator?: Company;
|
operator?: Company;
|
||||||
property_owner?: Company;
|
property_owner?: Company;
|
||||||
banner_image_url?: string;
|
banner_image_url?: string;
|
||||||
|
banner_image_id?: string;
|
||||||
card_image_url?: string;
|
card_image_url?: string;
|
||||||
|
card_image_id?: string;
|
||||||
average_rating: number;
|
average_rating: number;
|
||||||
review_count: number;
|
review_count: number;
|
||||||
ride_count: number;
|
ride_count: number;
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user