mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 10:26:59 -05:00
Compare commits
2 Commits
2deab69ebe
...
cb01707c5e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb01707c5e | ||
|
|
6b5be8a70b |
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@@ -35,6 +36,7 @@ interface DesignerFormProps {
|
|||||||
export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormProps): React.JSX.Element {
|
export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormProps): React.JSX.Element {
|
||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -75,6 +77,7 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const formData = {
|
const formData = {
|
||||||
...data,
|
...data,
|
||||||
@@ -97,6 +100,8 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
})} className="space-y-6">
|
})} className="space-y-6">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
@@ -274,12 +279,15 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Save Designer
|
Save Designer
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export function IntegrationTestRunner() {
|
|||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={runTests} disabled={isRunning || selectedSuites.length === 0}>
|
<Button onClick={runTests} loading={isRunning} loadingText="Running..." disabled={selectedSuites.length === 0}>
|
||||||
<Play className="w-4 h-4 mr-2" />
|
<Play className="w-4 h-4 mr-2" />
|
||||||
Run Selected
|
Run Selected
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@@ -37,6 +38,7 @@ interface ManufacturerFormProps {
|
|||||||
export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps): React.JSX.Element {
|
export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps): React.JSX.Element {
|
||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -79,6 +81,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const formData = {
|
const formData = {
|
||||||
...data,
|
...data,
|
||||||
@@ -86,7 +89,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|
||||||
// Only show success toast and close if not editing through moderation queue
|
// Only show success toast and close if not editing through moderation queue
|
||||||
if (!initialData?.id) {
|
if (!initialData?.id) {
|
||||||
@@ -101,6 +104,8 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
})} className="space-y-6">
|
})} className="space-y-6">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
@@ -284,12 +289,15 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Save Manufacturer
|
Save Manufacturer
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ export function NotificationDebugPanel() {
|
|||||||
<CardTitle>Notification Health Dashboard</CardTitle>
|
<CardTitle>Notification Health Dashboard</CardTitle>
|
||||||
<CardDescription>Monitor duplicate prevention and notification system health</CardDescription>
|
<CardDescription>Monitor duplicate prevention and notification system health</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={loadData} disabled={isLoading}>
|
<Button variant="outline" size="sm" onClick={loadData} loading={isLoading} loadingText="Loading...">
|
||||||
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,11 +107,11 @@ export function NovuMigrationUtility(): React.JSX.Element {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void runMigration()}
|
onClick={() => void runMigration()}
|
||||||
disabled={isRunning}
|
loading={isRunning}
|
||||||
|
loadingText="Migrating Users..."
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{isRunning && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
Start Migration
|
||||||
{isRunning ? 'Migrating Users...' : 'Start Migration'}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isRunning && totalUsers > 0 && (
|
{isRunning && totalUsers > 0 && (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@@ -35,6 +36,7 @@ interface OperatorFormProps {
|
|||||||
export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormProps): React.JSX.Element {
|
export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormProps): React.JSX.Element {
|
||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -75,6 +77,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const formData = {
|
const formData = {
|
||||||
...data,
|
...data,
|
||||||
@@ -82,7 +85,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|
||||||
// Only show success toast and close if not editing through moderation queue
|
// Only show success toast and close if not editing through moderation queue
|
||||||
if (!initialData?.id) {
|
if (!initialData?.id) {
|
||||||
@@ -97,6 +100,8 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
})} className="space-y-6">
|
})} className="space-y-6">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
@@ -274,12 +279,15 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Save Operator
|
Save Operator
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
}, [onSubmit]);
|
}, [onSubmit]);
|
||||||
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// Operator state
|
// Operator state
|
||||||
const [selectedOperatorId, setSelectedOperatorId] = useState<string>(initialData?.operator_id || '');
|
const [selectedOperatorId, setSelectedOperatorId] = useState<string>(initialData?.operator_id || '');
|
||||||
@@ -198,7 +199,8 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
}, [operatorIsOwner, selectedOperatorId, setValue]);
|
}, [operatorIsOwner, selectedOperatorId, setValue]);
|
||||||
|
|
||||||
|
|
||||||
const handleFormSubmit = async (data: ParkFormData) => {
|
const handleFormSubmit = async (data: ParkFormData) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// CRITICAL: Block new photo uploads on edits
|
// CRITICAL: Block new photo uploads on edits
|
||||||
if (isEditing && data.images?.uploaded) {
|
if (isEditing && data.images?.uploaded) {
|
||||||
@@ -277,6 +279,8 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -647,9 +651,17 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
{isEditing ? 'Update Park' : 'Create Park'}
|
{isEditing ? 'Update Park' : 'Create Park'}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
Save Park
|
||||||
|
</Button>
|
||||||
|
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@@ -35,6 +36,7 @@ interface PropertyOwnerFormProps {
|
|||||||
export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyOwnerFormProps): React.JSX.Element {
|
export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyOwnerFormProps): React.JSX.Element {
|
||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -75,6 +77,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const formData = {
|
const formData = {
|
||||||
...data,
|
...data,
|
||||||
@@ -82,7 +85,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|
||||||
// Only show success toast and close if not editing through moderation queue
|
// Only show success toast and close if not editing through moderation queue
|
||||||
if (!initialData?.id) {
|
if (!initialData?.id) {
|
||||||
@@ -97,6 +100,8 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
})} className="space-y-6">
|
})} className="space-y-6">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
@@ -274,12 +279,15 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Save Property Owner
|
Save Property Owner
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
const { preferences } = useUnitPreferences();
|
const { preferences } = useUnitPreferences();
|
||||||
const measurementSystem = preferences.measurement_system;
|
const measurementSystem = preferences.measurement_system;
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// Validate that onSubmit uses submission helpers (dev mode only)
|
// Validate that onSubmit uses submission helpers (dev mode only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -262,7 +263,8 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
const selectedCategory = watch('category');
|
const selectedCategory = watch('category');
|
||||||
|
|
||||||
|
|
||||||
const handleFormSubmit = async (data: RideFormData) => {
|
const handleFormSubmit = async (data: RideFormData) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// CRITICAL: Block new photo uploads on edits
|
// CRITICAL: Block new photo uploads on edits
|
||||||
if (isEditing && data.images?.uploaded) {
|
if (isEditing && data.images?.uploaded) {
|
||||||
@@ -355,6 +357,8 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1355,13 +1359,15 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
{isEditing ? 'Update Ride' : 'Create Ride'}
|
{isEditing ? 'Update Ride' : 'Create Ride'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import type { RideModelTechnicalSpec } from '@/types/database';
|
import type { RideModelTechnicalSpec } from '@/types/database';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
import { handleError } from '@/lib/errorHandler';
|
import { handleError } from '@/lib/errorHandler';
|
||||||
|
import { toast } from 'sonner';
|
||||||
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';
|
||||||
@@ -71,6 +72,7 @@ export function RideModelForm({
|
|||||||
initialData
|
initialData
|
||||||
}: RideModelFormProps) {
|
}: RideModelFormProps) {
|
||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [technicalSpecs, setTechnicalSpecs] = useState<{
|
const [technicalSpecs, setTechnicalSpecs] = useState<{
|
||||||
spec_name: string;
|
spec_name: string;
|
||||||
spec_value: string;
|
spec_value: string;
|
||||||
@@ -101,14 +103,16 @@ export function RideModelForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleFormSubmit = (data: RideModelFormData) => {
|
const handleFormSubmit = async (data: RideModelFormData) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// Include relational technical specs with extended type
|
// Include relational technical specs with extended type
|
||||||
onSubmit({
|
await onSubmit({
|
||||||
...data,
|
...data,
|
||||||
manufacturer_id: manufacturerId,
|
manufacturer_id: manufacturerId,
|
||||||
_technical_specifications: technicalSpecs
|
_technical_specifications: technicalSpecs
|
||||||
});
|
});
|
||||||
|
toast.success('Ride model submitted for review');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
handleError(error, {
|
handleError(error, {
|
||||||
action: initialData?.id ? 'Update Ride Model' : 'Create Ride Model'
|
action: initialData?.id ? 'Update Ride Model' : 'Create Ride Model'
|
||||||
@@ -116,6 +120,8 @@ export function RideModelForm({
|
|||||||
|
|
||||||
// Re-throw so parent can handle modal closing
|
// Re-throw so parent can handle modal closing
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,12 +300,15 @@ export function RideModelForm({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<X className="w-4 h-4 mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingText="Saving..."
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Save Model
|
Save Model
|
||||||
|
|||||||
@@ -777,9 +777,10 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={isRefreshing}
|
loading={isRefreshing}
|
||||||
|
loadingText="Refreshing..."
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
|
|||||||
@@ -435,7 +435,12 @@ export function TestDataGenerator(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button onClick={handleGenerate} disabled={loading || selectedEntityTypes.length === 0}>
|
<Button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
loading={loading}
|
||||||
|
loadingText="Generating..."
|
||||||
|
disabled={selectedEntityTypes.length === 0}
|
||||||
|
>
|
||||||
<Beaker className="w-4 h-4 mr-2" />
|
<Beaker className="w-4 h-4 mr-2" />
|
||||||
Generate Test Data
|
Generate Test Data
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ export function VersionCleanupSettings() {
|
|||||||
onChange={(e) => setRetentionDays(Number(e.target.value))}
|
onChange={(e) => setRetentionDays(Number(e.target.value))}
|
||||||
className="w-32"
|
className="w-32"
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSaveRetention} disabled={isSaving}>
|
<Button onClick={handleSaveRetention} loading={isSaving} loadingText="Saving...">
|
||||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Save'}
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Keep most recent 10 versions per item, delete older ones beyond this period
|
Keep most recent 10 versions per item, delete older ones beyond this period
|
||||||
@@ -176,15 +176,12 @@ export function VersionCleanupSettings() {
|
|||||||
<div className="pt-4 border-t">
|
<div className="pt-4 border-t">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleManualCleanup}
|
onClick={handleManualCleanup}
|
||||||
disabled={isLoading}
|
loading={isLoading}
|
||||||
|
loadingText="Running Cleanup..."
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
||||||
) : (
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
Run Manual Cleanup Now
|
Run Manual Cleanup Now
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||||
|
|||||||
@@ -31,32 +31,38 @@ interface AuthDiagnosticsData {
|
|||||||
export function AuthDiagnostics() {
|
export function AuthDiagnostics() {
|
||||||
const [diagnostics, setDiagnostics] = useState<AuthDiagnosticsData | null>(null);
|
const [diagnostics, setDiagnostics] = useState<AuthDiagnosticsData | null>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const runDiagnostics = async () => {
|
const runDiagnostics = async () => {
|
||||||
const storageStatus = authStorage.getStorageStatus();
|
setIsRefreshing(true);
|
||||||
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
|
try {
|
||||||
|
const storageStatus = authStorage.getStorageStatus();
|
||||||
|
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
storage: storageStatus,
|
storage: storageStatus,
|
||||||
session: {
|
session: {
|
||||||
exists: !!session,
|
exists: !!session,
|
||||||
user: session?.user?.email || null,
|
user: session?.user?.email || null,
|
||||||
expiresAt: session?.expires_at || null,
|
expiresAt: session?.expires_at || null,
|
||||||
error: sessionError?.message || null,
|
error: sessionError?.message || null,
|
||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
online: navigator.onLine,
|
online: navigator.onLine,
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
isIframe: window.self !== window.top,
|
isIframe: window.self !== window.top,
|
||||||
cookiesEnabled: navigator.cookieEnabled,
|
cookiesEnabled: navigator.cookieEnabled,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setDiagnostics(results);
|
setDiagnostics(results);
|
||||||
logger.debug('Auth diagnostics', { results });
|
logger.debug('Auth diagnostics', { results });
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -119,7 +125,7 @@ export function AuthDiagnostics() {
|
|||||||
⚠️ Running in iframe - storage may be restricted
|
⚠️ Running in iframe - storage may be restricted
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button onClick={runDiagnostics} variant="outline" size="sm" className="w-full mt-2">
|
<Button onClick={runDiagnostics} loading={isRefreshing} loadingText="Refreshing..." variant="outline" size="sm" className="w-full mt-2">
|
||||||
Refresh Diagnostics
|
Refresh Diagnostics
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function ConflictResolutionDialog({
|
|||||||
onResolve,
|
onResolve,
|
||||||
}: ConflictResolutionDialogProps) {
|
}: ConflictResolutionDialogProps) {
|
||||||
const [resolutions, setResolutions] = useState<Record<string, string>>({});
|
const [resolutions, setResolutions] = useState<Record<string, string>>({});
|
||||||
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const handleResolutionChange = (itemId: string, action: string) => {
|
const handleResolutionChange = (itemId: string, action: string) => {
|
||||||
@@ -44,6 +45,7 @@ export function ConflictResolutionDialog({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsApplying(true);
|
||||||
const { resolveConflicts } = await import('@/lib/conflictResolutionService');
|
const { resolveConflicts } = await import('@/lib/conflictResolutionService');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -67,6 +69,8 @@ export function ConflictResolutionDialog({
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
metadata: { conflictCount: conflicts.length }
|
metadata: { conflictCount: conflicts.length }
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsApplying(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,10 +123,10 @@ export function ConflictResolutionDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isApplying}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleApply} disabled={!allConflictsResolved}>
|
<Button onClick={handleApply} loading={isApplying} loadingText="Applying..." disabled={!allConflictsResolved}>
|
||||||
Apply & Approve
|
Apply & Approve
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -182,8 +182,8 @@ export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: A
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={submitting || !selectedRideId}>
|
<Button type="submit" loading={submitting} loadingText="Adding..." disabled={!selectedRideId}>
|
||||||
{submitting ? 'Adding...' : 'Add Credit'}
|
Add Credit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -199,9 +199,9 @@ export function ReviewForm({
|
|||||||
{/* Photo Upload */}
|
{/* Photo Upload */}
|
||||||
|
|
||||||
|
|
||||||
<Button type="submit" disabled={submitting} className="w-full">
|
<Button type="submit" loading={submitting} loadingText="Submitting..." className="w-full">
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
{submitting ? 'Submitting...' : 'Submit Review'}
|
Submit Review
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -363,8 +363,7 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }:
|
|||||||
<Button type="button" variant="outline" onClick={handleClose} disabled={loading}>
|
<Button type="button" variant="outline" onClick={handleClose} disabled={loading}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={loading || !captchaToken}>
|
<Button type="submit" loading={loading} loadingText="Changing Email..." disabled={!captchaToken}>
|
||||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Change Email
|
Change Email
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -558,8 +558,8 @@ export function LocationTab() {
|
|||||||
|
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" disabled={saving}>
|
<Button type="submit" loading={saving} loadingText="Saving...">
|
||||||
{saving ? 'Saving...' : 'Save Settings'}
|
Save Settings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -493,8 +493,7 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={verifyMFAAndUpdate} disabled={loading || totpCode.length !== 6}>
|
<Button onClick={verifyMFAAndUpdate} loading={loading} loadingText="Verifying..." disabled={totpCode.length !== 6}>
|
||||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Verify & Update
|
Verify & Update
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -450,8 +450,8 @@ export function PrivacyTab() {
|
|||||||
|
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" disabled={loading}>
|
<Button type="submit" loading={loading} loadingText="Saving...">
|
||||||
{loading ? 'Saving...' : 'Save Privacy Settings'}
|
Save Privacy Settings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
||||||
@@ -36,13 +37,33 @@ export interface ButtonProps
|
|||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
trackingLabel?: string; // Optional label for breadcrumb tracking
|
trackingLabel?: string; // Optional label for breadcrumb tracking
|
||||||
|
loading?: boolean; // Show loading state with spinner
|
||||||
|
loadingText?: string; // Optional text to display during loading
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, onClick, trackingLabel, ...props }, ref) => {
|
({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
onClick,
|
||||||
|
trackingLabel,
|
||||||
|
loading = false,
|
||||||
|
loadingText,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
// Prevent clicks while loading
|
||||||
|
if (loading) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add breadcrumb for button click
|
// Add breadcrumb for button click
|
||||||
if (trackingLabel) {
|
if (trackingLabel) {
|
||||||
breadcrumb.userAction('clicked', trackingLabel);
|
breadcrumb.userAction('clicked', trackingLabel);
|
||||||
@@ -57,8 +78,18 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...props}
|
disabled={disabled || loading}
|
||||||
/>
|
{...props}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
{loadingText || children}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Comp>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user