mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 13:31:22 -05:00
feat: Add button loading states
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -35,6 +36,7 @@ interface DesignerFormProps {
|
||||
export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormProps): React.JSX.Element {
|
||||
const { isModerator } = useUserRole();
|
||||
const { user } = useAuth();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -75,6 +77,7 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const formData = {
|
||||
...data,
|
||||
@@ -97,6 +100,8 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
})} className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
@@ -274,12 +279,15 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Designer
|
||||
|
||||
@@ -152,7 +152,7 @@ export function IntegrationTestRunner() {
|
||||
|
||||
{/* Controls */}
|
||||
<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" />
|
||||
Run Selected
|
||||
</Button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -37,6 +38,7 @@ interface ManufacturerFormProps {
|
||||
export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps): React.JSX.Element {
|
||||
const { isModerator } = useUserRole();
|
||||
const { user } = useAuth();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -79,6 +81,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const formData = {
|
||||
...data,
|
||||
@@ -86,7 +89,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
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
|
||||
if (!initialData?.id) {
|
||||
@@ -101,6 +104,8 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
})} className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
@@ -284,12 +289,15 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Manufacturer
|
||||
|
||||
@@ -142,8 +142,8 @@ export function NotificationDebugPanel() {
|
||||
<CardTitle>Notification Health Dashboard</CardTitle>
|
||||
<CardDescription>Monitor duplicate prevention and notification system health</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadData} disabled={isLoading}>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<Button variant="outline" size="sm" onClick={loadData} loading={isLoading} loadingText="Loading...">
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -35,6 +36,7 @@ interface OperatorFormProps {
|
||||
export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormProps): React.JSX.Element {
|
||||
const { isModerator } = useUserRole();
|
||||
const { user } = useAuth();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -75,6 +77,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const formData = {
|
||||
...data,
|
||||
@@ -82,7 +85,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
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
|
||||
if (!initialData?.id) {
|
||||
@@ -97,6 +100,8 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
})} className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
@@ -274,12 +279,15 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Operator
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -35,6 +36,7 @@ interface PropertyOwnerFormProps {
|
||||
export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyOwnerFormProps): React.JSX.Element {
|
||||
const { isModerator } = useUserRole();
|
||||
const { user } = useAuth();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -75,6 +77,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const formData = {
|
||||
...data,
|
||||
@@ -82,7 +85,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
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
|
||||
if (!initialData?.id) {
|
||||
@@ -97,6 +100,8 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
})} className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
@@ -274,12 +279,15 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Property Owner
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -71,6 +72,7 @@ export function RideModelForm({
|
||||
initialData
|
||||
}: RideModelFormProps) {
|
||||
const { isModerator } = useUserRole();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [technicalSpecs, setTechnicalSpecs] = useState<{
|
||||
spec_name: string;
|
||||
spec_value: string;
|
||||
@@ -101,14 +103,16 @@ export function RideModelForm({
|
||||
});
|
||||
|
||||
|
||||
const handleFormSubmit = (data: RideModelFormData) => {
|
||||
const handleFormSubmit = async (data: RideModelFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Include relational technical specs with extended type
|
||||
onSubmit({
|
||||
await onSubmit({
|
||||
...data,
|
||||
manufacturer_id: manufacturerId,
|
||||
_technical_specifications: technicalSpecs
|
||||
});
|
||||
toast.success('Ride model submitted for review');
|
||||
} catch (error: unknown) {
|
||||
handleError(error, {
|
||||
action: initialData?.id ? 'Update Ride Model' : 'Create Ride Model'
|
||||
@@ -116,6 +120,8 @@ export function RideModelForm({
|
||||
|
||||
// Re-throw so parent can handle modal closing
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -294,12 +300,15 @@ export function RideModelForm({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Model
|
||||
|
||||
@@ -777,9 +777,10 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
|
||||
variant="outline"
|
||||
size="sm"
|
||||
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
|
||||
</Button>
|
||||
{showFilters && (
|
||||
|
||||
@@ -435,7 +435,12 @@ export function TestDataGenerator(): React.JSX.Element {
|
||||
)}
|
||||
|
||||
<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" />
|
||||
Generate Test Data
|
||||
</Button>
|
||||
|
||||
@@ -148,8 +148,8 @@ export function VersionCleanupSettings() {
|
||||
onChange={(e) => setRetentionDays(Number(e.target.value))}
|
||||
className="w-32"
|
||||
/>
|
||||
<Button onClick={handleSaveRetention} disabled={isSaving}>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Save'}
|
||||
<Button onClick={handleSaveRetention} loading={isSaving} loadingText="Saving...">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -176,15 +176,12 @@ export function VersionCleanupSettings() {
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
onClick={handleManualCleanup}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
loadingText="Running Cleanup..."
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Run Manual Cleanup Now
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
||||
@@ -36,13 +37,33 @@ export interface ButtonProps
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
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>(
|
||||
({ 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 handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
// Prevent clicks while loading
|
||||
if (loading) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add breadcrumb for button click
|
||||
if (trackingLabel) {
|
||||
breadcrumb.userAction('clicked', trackingLabel);
|
||||
@@ -57,8 +78,18 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
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