mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:11:14 -05:00
Compare commits
3 Commits
7d085a0702
...
2468d3cc18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2468d3cc18 | ||
|
|
f4300de738 | ||
|
|
92e93bfc9d |
@@ -16,8 +16,9 @@ import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { HeadquartersLocationInput } from './HeadquartersLocationInput';
|
||||
import { EntityMultiImageUploader } from '@/components/upload/EntityMultiImageUploader';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { toast } from 'sonner';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import type { UploadedImage } from '@/types/company';
|
||||
|
||||
// Zod output type (after transformation)
|
||||
@@ -73,7 +74,7 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(async (data) => {
|
||||
if (!user) {
|
||||
toast.error('You must be logged in to submit');
|
||||
formToasts.error.generic('You must be logged in to submit');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,9 +94,11 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
||||
|
||||
await onSubmit(formData);
|
||||
|
||||
// Only show success toast and close if not editing through moderation queue
|
||||
if (!initialData?.id) {
|
||||
toast.success('Designer submitted for review');
|
||||
// Show success toast
|
||||
if (initialData?.id) {
|
||||
formToasts.success.update('Designer', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Designer', data.name);
|
||||
onCancel();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -104,6 +107,9 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
||||
metadata: { companyName: data.name }
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(getErrorMessage(error));
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -17,8 +17,9 @@ import { HeadquartersLocationInput } from './HeadquartersLocationInput';
|
||||
import { EntityMultiImageUploader } from '@/components/upload/EntityMultiImageUploader';
|
||||
import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { toast } from 'sonner';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils';
|
||||
import type { UploadedImage } from '@/types/company';
|
||||
|
||||
@@ -77,7 +78,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(async (data) => {
|
||||
if (!user) {
|
||||
toast.error('You must be logged in to submit');
|
||||
formToasts.error.generic('You must be logged in to submit');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,9 +96,11 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
|
||||
await onSubmit(formData);
|
||||
|
||||
// Only show success toast and close if not editing through moderation queue
|
||||
if (!initialData?.id) {
|
||||
toast.success('Manufacturer submitted for review');
|
||||
// Show success toast
|
||||
if (initialData?.id) {
|
||||
formToasts.success.update('Manufacturer', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Manufacturer', data.name);
|
||||
onCancel();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -106,6 +109,9 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
metadata: { companyName: data.name }
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(getErrorMessage(error));
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -16,8 +16,9 @@ import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { HeadquartersLocationInput } from './HeadquartersLocationInput';
|
||||
import { EntityMultiImageUploader } from '@/components/upload/EntityMultiImageUploader';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { toast } from 'sonner';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import type { UploadedImage } from '@/types/company';
|
||||
|
||||
// Zod output type (after transformation)
|
||||
@@ -73,7 +74,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(async (data) => {
|
||||
if (!user) {
|
||||
toast.error('You must be logged in to submit');
|
||||
formToasts.error.generic('You must be logged in to submit');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,9 +94,11 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
|
||||
await onSubmit(formData);
|
||||
|
||||
// Only show success toast and close if not editing through moderation queue
|
||||
if (!initialData?.id) {
|
||||
toast.success('Operator submitted for review');
|
||||
// Show success toast
|
||||
if (initialData?.id) {
|
||||
formToasts.success.update('Operator', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Operator', data.name);
|
||||
onCancel();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -104,6 +107,9 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
metadata: { companyName: data.name }
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(getErrorMessage(error));
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-
|
||||
import { SlugField } from '@/components/ui/slug-field';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import { MapPin, Save, X, Plus, AlertCircle, Info } from 'lucide-react';
|
||||
import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -294,7 +295,16 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
|
||||
await onSubmit(submissionData);
|
||||
|
||||
// Parent component handles success feedback
|
||||
// Show success toast
|
||||
if (isModerator()) {
|
||||
formToasts.success.moderatorApproval('Park', data.name);
|
||||
} else if (isEditing) {
|
||||
formToasts.success.update('Park', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Park', data.name);
|
||||
}
|
||||
|
||||
// Parent component handles modal closing/navigation
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
handleError(error, {
|
||||
@@ -308,6 +318,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
}
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(errorMessage);
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -16,8 +16,9 @@ import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { HeadquartersLocationInput } from './HeadquartersLocationInput';
|
||||
import { EntityMultiImageUploader } from '@/components/upload/EntityMultiImageUploader';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { toast } from 'sonner';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import type { UploadedImage } from '@/types/company';
|
||||
|
||||
// Zod output type (after transformation)
|
||||
@@ -73,7 +74,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(async (data) => {
|
||||
if (!user) {
|
||||
toast.error('You must be logged in to submit');
|
||||
formToasts.error.generic('You must be logged in to submit');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,9 +94,11 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
|
||||
await onSubmit(formData);
|
||||
|
||||
// Only show success toast and close if not editing through moderation queue
|
||||
if (!initialData?.id) {
|
||||
toast.success('Property owner submitted for review');
|
||||
// Show success toast
|
||||
if (initialData?.id) {
|
||||
formToasts.success.update('Property Owner', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Property Owner', data.name);
|
||||
onCancel();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -104,6 +107,9 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
metadata: { companyName: data.name }
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(getErrorMessage(error));
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import { Plus, Zap, Save, X, Building2, AlertCircle, Info, HelpCircle } from 'lucide-react';
|
||||
import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils';
|
||||
import { useUnitPreferences } from '@/hooks/useUnitPreferences';
|
||||
@@ -360,14 +361,14 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
// Pass clean data to parent with extended fields
|
||||
await onSubmit(metricData);
|
||||
|
||||
toast({
|
||||
title: isEditing ? "Ride Updated" : "Submission Sent",
|
||||
description: isEditing
|
||||
? "The ride information has been updated successfully."
|
||||
: tempNewManufacturer
|
||||
? "Ride, manufacturer, and model submitted for review"
|
||||
: "Ride submitted for review"
|
||||
});
|
||||
// Show success toast
|
||||
if (isModerator()) {
|
||||
formToasts.success.moderatorApproval('Ride', data.name);
|
||||
} else if (isEditing) {
|
||||
formToasts.success.update('Ride', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Ride', data.name);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
handleError(error, {
|
||||
action: isEditing ? 'Update Ride' : 'Create Ride',
|
||||
@@ -378,6 +379,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
}
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(getErrorMessage(error));
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button';
|
||||
import type { RideModelTechnicalSpec } from '@/types/database';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { toast } from 'sonner';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { formToasts } from '@/lib/formToasts';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -112,12 +113,21 @@ export function RideModelForm({
|
||||
manufacturer_id: manufacturerId,
|
||||
_technical_specifications: technicalSpecs
|
||||
});
|
||||
toast.success('Ride model submitted for review');
|
||||
|
||||
// Show success toast
|
||||
if (initialData?.id) {
|
||||
formToasts.success.update('Ride Model', data.name);
|
||||
} else {
|
||||
formToasts.success.create('Ride Model', data.name);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
handleError(error, {
|
||||
action: initialData?.id ? 'Update Ride Model' : 'Create Ride Model'
|
||||
});
|
||||
|
||||
// Show error toast
|
||||
formToasts.error.generic(getErrorMessage(error));
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -29,6 +29,11 @@ class AnalyticsErrorBoundary extends Component<
|
||||
}
|
||||
|
||||
export function AnalyticsWrapper() {
|
||||
// Disable analytics in development to reduce console noise
|
||||
if (import.meta.env.DEV) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnalyticsErrorBoundary>
|
||||
<Analytics />
|
||||
|
||||
@@ -75,6 +75,12 @@ interface FormFieldWrapperProps {
|
||||
|
||||
/** Hide automatic hint */
|
||||
hideHint?: boolean;
|
||||
|
||||
/** When to show validation feedback */
|
||||
validationMode?: 'realtime' | 'onBlur';
|
||||
|
||||
/** Callback when field is blurred (for onBlur mode) */
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,7 +198,10 @@ export function FormFieldWrapper({
|
||||
textareaProps,
|
||||
className,
|
||||
hideHint = false,
|
||||
validationMode = 'realtime',
|
||||
onBlur,
|
||||
}: FormFieldWrapperProps) {
|
||||
const [hasBlurred, setHasBlurred] = React.useState(false);
|
||||
const isTextarea = fieldType === 'textarea' || fieldType === 'submission-notes';
|
||||
const autoHint = getAutoHint(fieldType);
|
||||
const displayHint = hint || autoHint;
|
||||
@@ -203,12 +212,27 @@ export function FormFieldWrapper({
|
||||
const charCount = typeof value === 'string' ? value.length : 0;
|
||||
|
||||
// Determine validation state
|
||||
const shouldShowValidation = validationMode === 'realtime' || (validationMode === 'onBlur' && hasBlurred);
|
||||
const hasValue = value !== undefined && value !== null && value !== '';
|
||||
const isValid = !error && hasValue;
|
||||
const hasError = !!error;
|
||||
const isValid = shouldShowValidation && !error && hasValue;
|
||||
const hasError = shouldShowValidation && !!error;
|
||||
|
||||
// Blur handler
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setHasBlurred(true);
|
||||
if (validationMode === 'onBlur' && onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
// Call original onBlur if provided
|
||||
if ('value' in e.target && textareaProps?.onBlur) {
|
||||
textareaProps.onBlur(e as React.FocusEvent<HTMLTextAreaElement>);
|
||||
} else if (inputProps?.onBlur) {
|
||||
inputProps.onBlur(e as React.FocusEvent<HTMLInputElement>);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<div className={cn("space-y-2", className)} data-error={hasError ? "true" : undefined}>
|
||||
{/* Label with optional terminology tooltip */}
|
||||
<Label htmlFor={id} className="flex items-center gap-2">
|
||||
{termKey ? (
|
||||
@@ -232,11 +256,13 @@ export function FormFieldWrapper({
|
||||
<Textarea
|
||||
id={id}
|
||||
className={cn(
|
||||
"pr-10",
|
||||
error && "border-destructive",
|
||||
isValid && "border-green-500/50"
|
||||
"pr-10 transition-all duration-300 ease-in-out",
|
||||
"focus:ring-2 focus:ring-primary/20 focus:border-primary",
|
||||
error && "border-destructive focus:ring-destructive/20",
|
||||
isValid && "border-green-500/50 focus:ring-green-500/20"
|
||||
)}
|
||||
maxLength={maxLength}
|
||||
onBlur={handleBlur}
|
||||
{...textareaProps}
|
||||
/>
|
||||
) : (
|
||||
@@ -244,23 +270,25 @@ export function FormFieldWrapper({
|
||||
id={id}
|
||||
type={inputType}
|
||||
className={cn(
|
||||
"pr-10",
|
||||
error && "border-destructive",
|
||||
isValid && "border-green-500/50"
|
||||
"pr-10 transition-all duration-300 ease-in-out",
|
||||
"focus:ring-2 focus:ring-primary/20 focus:border-primary",
|
||||
error && "border-destructive focus:ring-destructive/20",
|
||||
isValid && "border-green-500/50 focus:ring-green-500/20"
|
||||
)}
|
||||
maxLength={maxLength}
|
||||
onBlur={handleBlur}
|
||||
{...inputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Validation icon */}
|
||||
{/* Validation icon with animation */}
|
||||
{(isValid || hasError) && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
{isValid && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500 animate-fade-in" />
|
||||
)}
|
||||
{hasError && (
|
||||
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||
<AlertCircle className="h-4 w-4 text-destructive animate-fade-in" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -268,7 +296,7 @@ export function FormFieldWrapper({
|
||||
|
||||
{/* Hint text (if not hidden and exists) */}
|
||||
{!hideHint && displayHint && !error && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground animate-slide-in-down">
|
||||
{displayHint}
|
||||
{showCharCount && ` (${charCount}/${maxLength} characters)`}
|
||||
</p>
|
||||
@@ -281,9 +309,9 @@ export function FormFieldWrapper({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{/* Error message with animation */}
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">
|
||||
<p className="text-sm text-destructive animate-slide-in-down">
|
||||
{error}
|
||||
{showCharCount && ` (${charCount}/${maxLength})`}
|
||||
</p>
|
||||
@@ -299,6 +327,7 @@ export const formFieldPresets = {
|
||||
websiteUrl: (props: Partial<FormFieldWrapperProps>) => ({
|
||||
fieldType: 'url' as FormFieldType,
|
||||
label: 'Website URL',
|
||||
validationMode: 'onBlur',
|
||||
...props,
|
||||
}),
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
import type { DatabaseStatistics } from '@/types/database-stats';
|
||||
|
||||
export function useAdminDatabaseStats() {
|
||||
const location = useLocation();
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
|
||||
return useQuery({
|
||||
queryKey: queryKeys.admin.databaseStats(),
|
||||
queryFn: async () => {
|
||||
@@ -15,7 +19,8 @@ export function useAdminDatabaseStats() {
|
||||
|
||||
return data as unknown as DatabaseStatistics;
|
||||
},
|
||||
enabled: isAdminPage, // Only run query on admin pages
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchInterval: 60 * 1000, // Auto-refetch every 60 seconds
|
||||
refetchInterval: isAdminPage ? 60 * 1000 : false, // Only refetch on admin pages
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
import type { RecentAddition } from '@/types/database-stats';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useRecentAdditions(limit: number = 50, entityTypeFilter?: string) {
|
||||
const location = useLocation();
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: queryKeys.admin.recentAdditions(limit),
|
||||
queryFn: async () => {
|
||||
@@ -18,8 +22,9 @@ export function useRecentAdditions(limit: number = 50, entityTypeFilter?: string
|
||||
|
||||
return data as unknown as RecentAddition[];
|
||||
},
|
||||
enabled: isAdminPage, // Only run query on admin pages
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
refetchInterval: 30 * 1000, // Auto-refetch every 30 seconds
|
||||
refetchInterval: isAdminPage ? 30 * 1000 : false, // Only refetch on admin pages
|
||||
});
|
||||
|
||||
// Set up real-time subscriptions
|
||||
@@ -51,7 +56,7 @@ export function useRecentAdditions(limit: number = 50, entityTypeFilter?: string
|
||||
.subscribe(),
|
||||
supabase
|
||||
.channel('recent_additions_photos')
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'entity_photos' }, () => {
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'photos' }, () => {
|
||||
query.refetch();
|
||||
})
|
||||
.subscribe(),
|
||||
|
||||
65
src/lib/formToasts.ts
Normal file
65
src/lib/formToasts.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
|
||||
/**
|
||||
* Standardized toast notifications for form submissions
|
||||
* Provides consistent success/error feedback across all forms
|
||||
*/
|
||||
export const formToasts = {
|
||||
success: {
|
||||
create: (entityType: string, entityName?: string) => {
|
||||
toast({
|
||||
title: '✓ Submission Created',
|
||||
description: entityName
|
||||
? `${entityName} has been submitted for review.`
|
||||
: `${entityType} has been submitted for review.`,
|
||||
variant: 'default',
|
||||
});
|
||||
},
|
||||
|
||||
update: (entityType: string, entityName?: string) => {
|
||||
toast({
|
||||
title: '✓ Update Submitted',
|
||||
description: entityName
|
||||
? `Changes to ${entityName} have been submitted for review.`
|
||||
: `${entityType} update has been submitted for review.`,
|
||||
variant: 'default',
|
||||
});
|
||||
},
|
||||
|
||||
moderatorApproval: (entityType: string, entityName?: string) => {
|
||||
toast({
|
||||
title: '✓ Published Successfully',
|
||||
description: entityName
|
||||
? `${entityName} is now live on the site.`
|
||||
: `${entityType} is now live on the site.`,
|
||||
variant: 'default',
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
error: {
|
||||
validation: (fieldCount: number) => {
|
||||
toast({
|
||||
title: 'Validation Failed',
|
||||
description: `Please fix ${fieldCount} error${fieldCount > 1 ? 's' : ''} before submitting.`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
|
||||
network: () => {
|
||||
toast({
|
||||
title: 'Connection Error',
|
||||
description: 'Unable to submit. Please check your connection and try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
|
||||
generic: (error: string) => {
|
||||
toast({
|
||||
title: 'Submission Failed',
|
||||
description: error,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,272 @@
|
||||
-- Fix get_database_statistics function to use correct table name 'photos' instead of 'entity_photos'
|
||||
CREATE OR REPLACE FUNCTION public.get_database_statistics()
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
STABLE SECURITY DEFINER
|
||||
SET search_path TO 'public'
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_stats jsonb;
|
||||
v_parks_total integer;
|
||||
v_parks_active integer;
|
||||
v_parks_historical integer;
|
||||
v_parks_7d integer;
|
||||
v_parks_30d integer;
|
||||
v_rides_total integer;
|
||||
v_rides_active integer;
|
||||
v_rides_historical integer;
|
||||
v_rides_7d integer;
|
||||
v_rides_30d integer;
|
||||
v_companies_total integer;
|
||||
v_manufacturers integer;
|
||||
v_operators integer;
|
||||
v_designers integer;
|
||||
v_companies_7d integer;
|
||||
v_companies_30d integer;
|
||||
v_ride_models_total integer;
|
||||
v_ride_models_7d integer;
|
||||
v_ride_models_30d integer;
|
||||
v_locations_total integer;
|
||||
v_timeline_events_total integer;
|
||||
v_photos_total integer;
|
||||
v_photos_7d integer;
|
||||
v_photos_30d integer;
|
||||
v_users_total integer;
|
||||
v_users_active_30d integer;
|
||||
v_submissions_pending integer;
|
||||
v_submissions_approved integer;
|
||||
v_submissions_rejected integer;
|
||||
BEGIN
|
||||
-- Parks statistics
|
||||
SELECT COUNT(*) INTO v_parks_total FROM parks;
|
||||
SELECT COUNT(*) INTO v_parks_active FROM parks WHERE status = 'operating';
|
||||
SELECT COUNT(*) INTO v_parks_historical FROM parks WHERE status IN ('closed', 'historical');
|
||||
SELECT COUNT(*) INTO v_parks_7d FROM parks WHERE created_at > NOW() - INTERVAL '7 days';
|
||||
SELECT COUNT(*) INTO v_parks_30d FROM parks WHERE created_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Rides statistics
|
||||
SELECT COUNT(*) INTO v_rides_total FROM rides;
|
||||
SELECT COUNT(*) INTO v_rides_active FROM rides WHERE status = 'operating';
|
||||
SELECT COUNT(*) INTO v_rides_historical FROM rides WHERE status IN ('closed', 'removed', 'relocated');
|
||||
SELECT COUNT(*) INTO v_rides_7d FROM rides WHERE created_at > NOW() - INTERVAL '7 days';
|
||||
SELECT COUNT(*) INTO v_rides_30d FROM rides WHERE created_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Companies statistics
|
||||
SELECT COUNT(*) INTO v_companies_total FROM companies;
|
||||
SELECT COUNT(*) INTO v_manufacturers FROM companies WHERE company_type = 'manufacturer';
|
||||
SELECT COUNT(*) INTO v_operators FROM companies WHERE company_type = 'operator';
|
||||
SELECT COUNT(*) INTO v_designers FROM companies WHERE company_type = 'designer';
|
||||
SELECT COUNT(*) INTO v_companies_7d FROM companies WHERE created_at > NOW() - INTERVAL '7 days';
|
||||
SELECT COUNT(*) INTO v_companies_30d FROM companies WHERE created_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Ride models statistics
|
||||
SELECT COUNT(*) INTO v_ride_models_total FROM ride_models;
|
||||
SELECT COUNT(*) INTO v_ride_models_7d FROM ride_models WHERE created_at > NOW() - INTERVAL '7 days';
|
||||
SELECT COUNT(*) INTO v_ride_models_30d FROM ride_models WHERE created_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Locations statistics
|
||||
SELECT COUNT(*) INTO v_locations_total FROM locations;
|
||||
|
||||
-- Timeline events statistics
|
||||
SELECT COUNT(*) INTO v_timeline_events_total FROM entity_timeline_events;
|
||||
|
||||
-- Photos statistics - FIXED: using 'photos' table instead of 'entity_photos'
|
||||
SELECT COUNT(*) INTO v_photos_total FROM photos;
|
||||
SELECT COUNT(*) INTO v_photos_7d FROM photos WHERE created_at > NOW() - INTERVAL '7 days';
|
||||
SELECT COUNT(*) INTO v_photos_30d FROM photos WHERE created_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Users statistics
|
||||
SELECT COUNT(*) INTO v_users_total FROM profiles;
|
||||
SELECT COUNT(*) INTO v_users_active_30d FROM profiles WHERE updated_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Submissions statistics
|
||||
SELECT COUNT(*) INTO v_submissions_pending FROM content_submissions WHERE status = 'pending';
|
||||
SELECT COUNT(*) INTO v_submissions_approved FROM content_submissions WHERE status = 'approved';
|
||||
SELECT COUNT(*) INTO v_submissions_rejected FROM content_submissions WHERE status = 'rejected';
|
||||
|
||||
-- Build result JSON
|
||||
v_stats := jsonb_build_object(
|
||||
'parks', jsonb_build_object(
|
||||
'total', v_parks_total,
|
||||
'active', v_parks_active,
|
||||
'historical', v_parks_historical,
|
||||
'added_7d', v_parks_7d,
|
||||
'added_30d', v_parks_30d
|
||||
),
|
||||
'rides', jsonb_build_object(
|
||||
'total', v_rides_total,
|
||||
'active', v_rides_active,
|
||||
'historical', v_rides_historical,
|
||||
'added_7d', v_rides_7d,
|
||||
'added_30d', v_rides_30d
|
||||
),
|
||||
'companies', jsonb_build_object(
|
||||
'total', v_companies_total,
|
||||
'manufacturers', v_manufacturers,
|
||||
'operators', v_operators,
|
||||
'designers', v_designers,
|
||||
'added_7d', v_companies_7d,
|
||||
'added_30d', v_companies_30d
|
||||
),
|
||||
'ride_models', jsonb_build_object(
|
||||
'total', v_ride_models_total,
|
||||
'added_7d', v_ride_models_7d,
|
||||
'added_30d', v_ride_models_30d
|
||||
),
|
||||
'locations', jsonb_build_object(
|
||||
'total', v_locations_total
|
||||
),
|
||||
'timeline_events', jsonb_build_object(
|
||||
'total', v_timeline_events_total
|
||||
),
|
||||
'photos', jsonb_build_object(
|
||||
'total', v_photos_total,
|
||||
'added_7d', v_photos_7d,
|
||||
'added_30d', v_photos_30d
|
||||
),
|
||||
'users', jsonb_build_object(
|
||||
'total', v_users_total,
|
||||
'active_30d', v_users_active_30d
|
||||
),
|
||||
'submissions', jsonb_build_object(
|
||||
'pending', v_submissions_pending,
|
||||
'approved', v_submissions_approved,
|
||||
'rejected', v_submissions_rejected
|
||||
)
|
||||
);
|
||||
|
||||
RETURN v_stats;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
-- Fix get_recent_additions function to use correct table and column names
|
||||
CREATE OR REPLACE FUNCTION public.get_recent_additions(limit_count integer DEFAULT 50)
|
||||
RETURNS TABLE(entity_id uuid, entity_type text, entity_name text, entity_slug text, park_slug text, image_url text, created_at timestamp with time zone, created_by_id uuid, created_by_username text, created_by_avatar text)
|
||||
LANGUAGE plpgsql
|
||||
STABLE SECURITY DEFINER
|
||||
SET search_path TO 'public'
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT * FROM (
|
||||
-- Parks
|
||||
SELECT
|
||||
p.id as entity_id,
|
||||
'park'::text as entity_type,
|
||||
p.name as entity_name,
|
||||
p.slug as entity_slug,
|
||||
NULL::text as park_slug,
|
||||
p.card_image_url as image_url,
|
||||
p.created_at,
|
||||
p.created_by as created_by_id,
|
||||
prof.username as created_by_username,
|
||||
prof.avatar_url as created_by_avatar
|
||||
FROM parks p
|
||||
LEFT JOIN profiles prof ON prof.user_id = p.created_by
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Rides
|
||||
SELECT
|
||||
r.id as entity_id,
|
||||
'ride'::text as entity_type,
|
||||
r.name as entity_name,
|
||||
r.slug as entity_slug,
|
||||
pk.slug as park_slug,
|
||||
r.card_image_url as image_url,
|
||||
r.created_at,
|
||||
r.created_by as created_by_id,
|
||||
prof.username as created_by_username,
|
||||
prof.avatar_url as created_by_avatar
|
||||
FROM rides r
|
||||
LEFT JOIN parks pk ON pk.id = r.park_id
|
||||
LEFT JOIN profiles prof ON prof.user_id = r.created_by
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Companies
|
||||
SELECT
|
||||
c.id as entity_id,
|
||||
'company'::text as entity_type,
|
||||
c.name as entity_name,
|
||||
c.slug as entity_slug,
|
||||
NULL::text as park_slug,
|
||||
c.card_image_url as image_url,
|
||||
c.created_at,
|
||||
c.created_by as created_by_id,
|
||||
prof.username as created_by_username,
|
||||
prof.avatar_url as created_by_avatar
|
||||
FROM companies c
|
||||
LEFT JOIN profiles prof ON prof.user_id = c.created_by
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Ride Models
|
||||
SELECT
|
||||
rm.id as entity_id,
|
||||
'ride_model'::text as entity_type,
|
||||
rm.name as entity_name,
|
||||
rm.slug as entity_slug,
|
||||
NULL::text as park_slug,
|
||||
rm.card_image_url as image_url,
|
||||
rm.created_at,
|
||||
rm.created_by as created_by_id,
|
||||
prof.username as created_by_username,
|
||||
prof.avatar_url as created_by_avatar
|
||||
FROM ride_models rm
|
||||
LEFT JOIN profiles prof ON prof.user_id = rm.created_by
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Locations
|
||||
SELECT
|
||||
l.id as entity_id,
|
||||
'location'::text as entity_type,
|
||||
COALESCE(l.city || ', ' || l.country, l.country, 'Location') as entity_name,
|
||||
NULL::text as entity_slug,
|
||||
NULL::text as park_slug,
|
||||
NULL::text as image_url,
|
||||
l.created_at,
|
||||
NULL::uuid as created_by_id,
|
||||
NULL::text as created_by_username,
|
||||
NULL::text as created_by_avatar
|
||||
FROM locations l
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Timeline Events
|
||||
SELECT
|
||||
te.id as entity_id,
|
||||
'timeline_event'::text as entity_type,
|
||||
te.event_title as entity_name,
|
||||
NULL::text as entity_slug,
|
||||
NULL::text as park_slug,
|
||||
NULL::text as image_url,
|
||||
te.created_at,
|
||||
te.created_by as created_by_id,
|
||||
prof.username as created_by_username,
|
||||
prof.avatar_url as created_by_avatar
|
||||
FROM entity_timeline_events te
|
||||
LEFT JOIN profiles prof ON prof.user_id = te.created_by
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Photos - FIXED: using 'photos' table and correct column names
|
||||
SELECT
|
||||
p.id as entity_id,
|
||||
'photo'::text as entity_type,
|
||||
COALESCE(p.title, 'Photo') as entity_name,
|
||||
NULL::text as entity_slug,
|
||||
NULL::text as park_slug,
|
||||
p.cloudflare_image_url as image_url,
|
||||
p.created_at as created_at,
|
||||
p.submitted_by as created_by_id,
|
||||
prof.username as created_by_username,
|
||||
prof.avatar_url as created_by_avatar
|
||||
FROM photos p
|
||||
LEFT JOIN profiles prof ON prof.user_id = p.submitted_by
|
||||
) combined
|
||||
ORDER BY created_at DESC
|
||||
LIMIT limit_count;
|
||||
END;
|
||||
$function$;
|
||||
@@ -104,11 +104,26 @@ export default {
|
||||
"0%": { transform: "translateX(-100%)" },
|
||||
"100%": { transform: "translateX(100%)" },
|
||||
},
|
||||
"fade-in": {
|
||||
"0%": { opacity: "0", transform: "translateY(-4px)" },
|
||||
"100%": { opacity: "1", transform: "translateY(0)" }
|
||||
},
|
||||
"fade-out": {
|
||||
"0%": { opacity: "1", transform: "translateY(0)" },
|
||||
"100%": { opacity: "0", transform: "translateY(-4px)" }
|
||||
},
|
||||
"slide-in-down": {
|
||||
"0%": { opacity: "0", transform: "translateY(-8px)" },
|
||||
"100%": { opacity: "1", transform: "translateY(0)" }
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
shimmer: "shimmer 2s infinite",
|
||||
"fade-in": "fade-in 0.2s ease-out",
|
||||
"fade-out": "fade-out 0.2s ease-out",
|
||||
"slide-in-down": "slide-in-down 0.3s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user