feat: Add button loading states

This commit is contained in:
gpt-engineer-app[bot]
2025-11-04 18:11:31 +00:00
parent 2deab69ebe
commit 6b5be8a70b
11 changed files with 98 additions and 23 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>
);
},
);