mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 09:46:59 -05:00
Compare commits
2 Commits
7476fbd5da
...
524f6a65e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
524f6a65e8 | ||
|
|
fa3dfcfdee |
@@ -20,6 +20,8 @@ import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
||||
import { handleError } from "@/lib/errorHandler";
|
||||
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
|
||||
import { APIStatusBanner } from "@/components/ui/api-status-banner";
|
||||
import { useAdminRoutePreload } from "@/hooks/useAdminRoutePreload";
|
||||
import { useVersionCheck } from "@/hooks/useVersionCheck";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Core routes (eager-loaded for best UX)
|
||||
@@ -136,6 +138,12 @@ function AppContent(): React.JSX.Element {
|
||||
// Check if API status banner is visible to add padding
|
||||
const { isAPIReachable, isBannerDismissed } = useAPIConnectivity();
|
||||
const showBanner = !isAPIReachable && !isBannerDismissed;
|
||||
|
||||
// Preload admin routes for moderators/admins
|
||||
useAdminRoutePreload();
|
||||
|
||||
// Monitor for new deployments
|
||||
useVersionCheck();
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -71,6 +71,32 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleClearCacheAndReload = async () => {
|
||||
try {
|
||||
// Clear all caches
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
||||
}
|
||||
|
||||
// Unregister service workers
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map(reg => reg.unregister()));
|
||||
}
|
||||
|
||||
// Clear session storage chunk reload flag
|
||||
sessionStorage.removeItem('chunk-load-reload');
|
||||
|
||||
// Force reload bypassing cache
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
// Fallback to regular reload if cache clearing fails
|
||||
console.error('Failed to clear cache:', error);
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
handleGoHome = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
@@ -90,12 +116,23 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
||||
<AlertTriangle className="w-8 h-8 text-destructive" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">
|
||||
{isChunkError ? 'New Version Available' : 'Something Went Wrong'}
|
||||
{isChunkError ? 'App Update Required' : 'Something Went Wrong'}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
{isChunkError
|
||||
? "The app has been updated. Please reload the page to get the latest version."
|
||||
: "We encountered an unexpected error. This has been logged and we'll look into it."}
|
||||
<CardDescription className="mt-2 space-y-2">
|
||||
{isChunkError ? (
|
||||
<>
|
||||
<p>The app has been updated with new features and improvements.</p>
|
||||
<p className="text-sm font-medium">
|
||||
To continue, please clear your browser cache and reload:
|
||||
</p>
|
||||
<ul className="text-sm list-disc list-inside space-y-1 ml-2">
|
||||
<li>Click "Clear Cache & Reload" below, or</li>
|
||||
<li>Press <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Ctrl+Shift+R</kbd> (Windows/Linux) or <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">⌘+Shift+R</kbd> (Mac)</li>
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
"We encountered an unexpected error. This has been logged and we'll look into it."
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -114,23 +151,35 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={this.handleReload}
|
||||
className="flex-1 gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Reload Page
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={this.handleGoHome}
|
||||
className="flex-1 gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Go Home
|
||||
</Button>
|
||||
<div className="flex flex-col gap-2">
|
||||
{isChunkError && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={this.handleClearCacheAndReload}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Clear Cache & Reload
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant={isChunkError ? "outline" : "default"}
|
||||
onClick={this.handleReload}
|
||||
className="flex-1 gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Reload Page
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={this.handleGoHome}
|
||||
className="flex-1 gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Go Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
|
||||
@@ -22,6 +22,7 @@ import { jsonToFormData } from '@/lib/typeConversions';
|
||||
import { PropertyOwnerForm } from '@/components/admin/PropertyOwnerForm';
|
||||
import { RideModelForm } from '@/components/admin/RideModelForm';
|
||||
import { Save, X, Edit } from 'lucide-react';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
interface ItemEditDialogProps {
|
||||
item?: SubmissionItemWithDeps | null;
|
||||
@@ -131,66 +132,70 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }:
|
||||
switch (editItem.item_type) {
|
||||
case 'park':
|
||||
return (
|
||||
<ParkForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// Convert Json to form-compatible object (null → undefined)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
isEditing
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<ParkForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
isEditing
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'ride':
|
||||
return (
|
||||
<RideForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// Convert Json to form-compatible object (null → undefined)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
isEditing
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<RideForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
isEditing
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'manufacturer':
|
||||
return (
|
||||
<ManufacturerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<ManufacturerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'designer':
|
||||
return (
|
||||
<DesignerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<DesignerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'operator':
|
||||
return (
|
||||
<OperatorForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<OperatorForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'property_owner':
|
||||
return (
|
||||
<PropertyOwnerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<PropertyOwnerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={jsonToFormData(editItem.item_data) as any}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'ride_model':
|
||||
@@ -201,14 +206,15 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }:
|
||||
? itemData.manufacturer_id
|
||||
: '';
|
||||
return (
|
||||
<RideModelForm
|
||||
manufacturerName={manufacturerName}
|
||||
manufacturerId={manufacturerId}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialData={itemData as any}
|
||||
/>
|
||||
<SubmissionErrorBoundary submissionId={editItem.id}>
|
||||
<RideModelForm
|
||||
manufacturerName={manufacturerName}
|
||||
manufacturerId={manufacturerId}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={itemData as any}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
);
|
||||
|
||||
case 'photo':
|
||||
|
||||
39
src/hooks/useAdminRoutePreload.ts
Normal file
39
src/hooks/useAdminRoutePreload.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from './useAuth';
|
||||
import { useUserRole } from './useUserRole';
|
||||
|
||||
/**
|
||||
* Preloads admin route chunks for authenticated moderators/admins
|
||||
* This reduces chunk load failures by warming up the browser cache
|
||||
*/
|
||||
export function useAdminRoutePreload() {
|
||||
const { user } = useAuth();
|
||||
const { isModerator, isAdmin } = useUserRole();
|
||||
|
||||
useEffect(() => {
|
||||
// Only preload if user has admin access
|
||||
if (!user || (!isModerator && !isAdmin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Preload admin chunks after a short delay to avoid blocking initial page load
|
||||
const preloadTimer = setTimeout(() => {
|
||||
// Preload critical admin routes
|
||||
const adminRoutes = [
|
||||
() => import('../pages/AdminDashboard'),
|
||||
() => import('../pages/AdminModeration'),
|
||||
() => import('../pages/AdminReports'),
|
||||
];
|
||||
|
||||
// Start preloading (but don't await - let it happen in background)
|
||||
adminRoutes.forEach(route => {
|
||||
route().catch(err => {
|
||||
// Silently fail - preloading is a performance optimization
|
||||
console.debug('Admin route preload failed:', err);
|
||||
});
|
||||
});
|
||||
}, 2000); // Wait 2 seconds after auth to avoid blocking initial render
|
||||
|
||||
return () => clearTimeout(preloadTimer);
|
||||
}, [user, isModerator, isAdmin]);
|
||||
}
|
||||
76
src/hooks/useVersionCheck.ts
Normal file
76
src/hooks/useVersionCheck.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// App version - automatically updated during build
|
||||
const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'dev';
|
||||
const VERSION_CHECK_INTERVAL = 5 * 60 * 1000; // Check every 5 minutes
|
||||
|
||||
/**
|
||||
* Monitors for new app deployments and prompts user to refresh
|
||||
*/
|
||||
export function useVersionCheck() {
|
||||
const [newVersionAvailable, setNewVersionAvailable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't run in development
|
||||
if (import.meta.env.DEV) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkVersion = async () => {
|
||||
try {
|
||||
// Fetch the current index.html with cache bypass
|
||||
const response = await fetch('/', {
|
||||
method: 'HEAD',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
},
|
||||
});
|
||||
|
||||
// Check ETag or Last-Modified to detect changes
|
||||
const etag = response.headers.get('ETag');
|
||||
const lastModified = response.headers.get('Last-Modified');
|
||||
|
||||
const currentFingerprint = `${etag}-${lastModified}`;
|
||||
const storedFingerprint = sessionStorage.getItem('app-version-fingerprint');
|
||||
|
||||
if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
||||
// New version detected
|
||||
setNewVersionAvailable(true);
|
||||
|
||||
toast.info('New version available', {
|
||||
description: 'A new version of ThrillWiki is available. Please refresh to update.',
|
||||
duration: 30000, // Show for 30 seconds
|
||||
action: {
|
||||
label: 'Refresh Now',
|
||||
onClick: () => window.location.reload(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Store current fingerprint
|
||||
if (!storedFingerprint) {
|
||||
sessionStorage.setItem('app-version-fingerprint', currentFingerprint);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - version check is non-critical
|
||||
console.debug('Version check failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check after 1 minute (give time for user to settle in)
|
||||
const initialTimer = setTimeout(checkVersion, 60000);
|
||||
|
||||
// Then check periodically
|
||||
const interval = setInterval(checkVersion, VERSION_CHECK_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearTimeout(initialTimer);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { newVersionAvailable };
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { handleNonCriticalError } from '@/lib/errorHandler';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function DesignerRides() {
|
||||
const { designerSlug } = useParams<{ designerSlug: string }>();
|
||||
@@ -329,10 +330,12 @@ export default function DesignerRides() {
|
||||
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function Designers() {
|
||||
useDocumentTitle('Designers');
|
||||
@@ -359,7 +360,9 @@ export default function Designers() {
|
||||
</main>
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DesignerForm onSubmit={handleCreateSubmit} onCancel={() => setIsCreateModalOpen(false)} />
|
||||
<SubmissionErrorBoundary>
|
||||
<DesignerForm onSubmit={handleCreateSubmit} onCancel={() => setIsCreateModalOpen(false)} />
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { handleNonCriticalError } from '@/lib/errorHandler';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
interface RideModelWithCount extends RideModel {
|
||||
ride_count: number;
|
||||
@@ -295,11 +296,13 @@ export default function ManufacturerModels() {
|
||||
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<RideModelForm
|
||||
manufacturerName={manufacturer.name}
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideModelForm
|
||||
manufacturerName={manufacturer.name}
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { handleNonCriticalError } from '@/lib/errorHandler';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function ManufacturerRides() {
|
||||
const { manufacturerSlug } = useParams<{ manufacturerSlug: string }>();
|
||||
@@ -329,10 +330,12 @@ export default function ManufacturerRides() {
|
||||
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function Manufacturers() {
|
||||
useDocumentTitle('Manufacturers');
|
||||
@@ -374,10 +375,12 @@ export default function Manufacturers() {
|
||||
{/* Create Modal */}
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<ManufacturerForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<ManufacturerForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
const Operators = () => {
|
||||
useDocumentTitle('Operators');
|
||||
@@ -377,10 +378,12 @@ const Operators = () => {
|
||||
{/* Create Modal */}
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<OperatorForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<OperatorForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { usePhotoCount } from '@/hooks/photos/usePhotoCount';
|
||||
const RideForm = lazy(() => import('@/components/admin/RideForm').then(m => ({ default: m.RideForm })));
|
||||
const ParkForm = lazy(() => import('@/components/admin/ParkForm').then(m => ({ default: m.ParkForm })));
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { Edit } from 'lucide-react';
|
||||
import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
||||
@@ -610,10 +611,13 @@ export default function ParkDetail() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Suspense fallback={<AdminFormSkeleton />}>
|
||||
<RideForm
|
||||
onSubmit={handleRideSubmit}
|
||||
onCancel={() => setIsAddRideModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideForm
|
||||
onSubmit={handleRideSubmit}
|
||||
onCancel={() => setIsAddRideModalOpen(false)}
|
||||
initialData={{ park_id: park.id }}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -628,28 +632,30 @@ export default function ParkDetail() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Suspense fallback={<AdminFormSkeleton />}>
|
||||
<ParkForm
|
||||
onSubmit={handleEditParkSubmit}
|
||||
onCancel={() => setIsEditParkModalOpen(false)}
|
||||
initialData={{
|
||||
id: park?.id,
|
||||
name: park?.name,
|
||||
slug: park?.slug,
|
||||
description: park?.description ?? undefined,
|
||||
park_type: park?.park_type,
|
||||
status: park?.status,
|
||||
opening_date: park?.opening_date ?? undefined,
|
||||
closing_date: park?.closing_date ?? undefined,
|
||||
website_url: park?.website_url ?? undefined,
|
||||
phone: park?.phone ?? undefined,
|
||||
email: park?.email ?? undefined,
|
||||
operator_id: park?.operator?.id,
|
||||
property_owner_id: park?.property_owner?.id,
|
||||
banner_image_url: park?.banner_image_url ?? undefined,
|
||||
card_image_url: park?.card_image_url ?? undefined
|
||||
}}
|
||||
isEditing={true}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<ParkForm
|
||||
onSubmit={handleEditParkSubmit}
|
||||
onCancel={() => setIsEditParkModalOpen(false)}
|
||||
initialData={{
|
||||
id: park?.id,
|
||||
name: park?.name,
|
||||
slug: park?.slug,
|
||||
description: park?.description ?? undefined,
|
||||
park_type: park?.park_type,
|
||||
status: park?.status,
|
||||
opening_date: park?.opening_date ?? undefined,
|
||||
closing_date: park?.closing_date ?? undefined,
|
||||
website_url: park?.website_url ?? undefined,
|
||||
phone: park?.phone ?? undefined,
|
||||
email: park?.email ?? undefined,
|
||||
operator_id: park?.operator?.id,
|
||||
property_owner_id: park?.property_owner?.id,
|
||||
banner_image_url: park?.banner_image_url ?? undefined,
|
||||
card_image_url: park?.card_image_url ?? undefined
|
||||
}}
|
||||
isEditing={true}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
const ParkOwners = () => {
|
||||
useDocumentTitle('Property Owners');
|
||||
@@ -242,10 +243,12 @@ const ParkOwners = () => {
|
||||
{/* Create Modal */}
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<PropertyOwnerForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<PropertyOwnerForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
|
||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function ParkRides() {
|
||||
const { parkSlug } = useParams<{ parkSlug: string }>();
|
||||
@@ -353,12 +354,14 @@ export default function ParkRides() {
|
||||
{/* Create Modal */}
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit as any}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
isEditing={false}
|
||||
initialData={{ park_id: park.id }}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit as any}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
isEditing={false}
|
||||
initialData={{ park_id: park.id }}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
|
||||
@@ -39,6 +39,7 @@ import { useAuthModal } from "@/hooks/useAuthModal";
|
||||
import { useOpenGraph } from "@/hooks/useOpenGraph";
|
||||
import { useParks } from "@/hooks/parks/useParks";
|
||||
import { Pagination } from "@/components/common/Pagination";
|
||||
import { SubmissionErrorBoundary } from "@/components/error/SubmissionErrorBoundary";
|
||||
|
||||
export interface FilterState {
|
||||
search: string;
|
||||
@@ -583,7 +584,9 @@ export default function Parks() {
|
||||
Add a new park to the database. Your submission will be reviewed before being published.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ParkForm onSubmit={handleParkSubmit} onCancel={() => setIsAddParkModalOpen(false)} isEditing={false} />
|
||||
<SubmissionErrorBoundary>
|
||||
<ParkForm onSubmit={handleParkSubmit} onCancel={() => setIsAddParkModalOpen(false)} isEditing={false} />
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { toast } from '@/hooks/use-toast';
|
||||
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
|
||||
import type { Ride, Company, RideModel } from "@/types/database";
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function RideModelRides() {
|
||||
const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>();
|
||||
@@ -293,10 +294,12 @@ export default function RideModelRides() {
|
||||
{/* Create Modal */}
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { useRides } from '@/hooks/rides/useRides';
|
||||
import { Pagination } from '@/components/common/Pagination';
|
||||
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
||||
|
||||
export default function Rides() {
|
||||
useDocumentTitle('Rides & Attractions');
|
||||
@@ -529,11 +530,13 @@ export default function Rides() {
|
||||
{/* Create Modal */}
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
isEditing={false}
|
||||
/>
|
||||
<SubmissionErrorBoundary>
|
||||
<RideForm
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setIsCreateModalOpen(false)}
|
||||
isEditing={false}
|
||||
/>
|
||||
</SubmissionErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user