feat: Implement button loading states

This commit is contained in:
gpt-engineer-app[bot]
2025-11-04 18:19:52 +00:00
parent 6b5be8a70b
commit cb01707c5e
11 changed files with 71 additions and 45 deletions

View File

@@ -107,11 +107,11 @@ export function NovuMigrationUtility(): React.JSX.Element {
<Button
onClick={() => void runMigration()}
disabled={isRunning}
loading={isRunning}
loadingText="Migrating Users..."
className="w-full"
>
{isRunning && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isRunning ? 'Migrating Users...' : 'Start Migration'}
Start Migration
</Button>
{isRunning && totalUsers > 0 && (

View File

@@ -140,6 +140,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
}, [onSubmit]);
const { user } = useAuth();
const [isSubmitting, setIsSubmitting] = useState(false);
// Operator state
const [selectedOperatorId, setSelectedOperatorId] = useState<string>(initialData?.operator_id || '');
@@ -198,7 +199,8 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
}, [operatorIsOwner, selectedOperatorId, setValue]);
const handleFormSubmit = async (data: ParkFormData) => {
const handleFormSubmit = async (data: ParkFormData) => {
setIsSubmitting(true);
try {
// CRITICAL: Block new photo uploads on edits
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
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" />
{isEditing ? 'Update Park' : 'Create Park'}
</Button>
<Button
type="submit"
loading={isSubmitting}
loadingText="Saving..."
>
<Save className="w-4 h-4 mr-2" />
Save Park
</Button>
{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" />
Cancel
</Button>

View File

@@ -158,6 +158,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
const { isModerator } = useUserRole();
const { preferences } = useUnitPreferences();
const measurementSystem = preferences.measurement_system;
const [isSubmitting, setIsSubmitting] = useState(false);
// Validate that onSubmit uses submission helpers (dev mode only)
useEffect(() => {
@@ -262,7 +263,8 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
const selectedCategory = watch('category');
const handleFormSubmit = async (data: RideFormData) => {
const handleFormSubmit = async (data: RideFormData) => {
setIsSubmitting(true);
try {
// CRITICAL: Block new photo uploads on edits
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
throw error;
} finally {
setIsSubmitting(false);
}
};
@@ -1355,13 +1359,15 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
<Button
type="submit"
className="flex-1"
loading={isSubmitting}
loadingText="Saving..."
>
<Save className="w-4 h-4 mr-2" />
{isEditing ? 'Update Ride' : 'Create Ride'}
</Button>
{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" />
Cancel
</Button>

View File

@@ -31,32 +31,38 @@ interface AuthDiagnosticsData {
export function AuthDiagnostics() {
const [diagnostics, setDiagnostics] = useState<AuthDiagnosticsData | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const runDiagnostics = async () => {
const storageStatus = authStorage.getStorageStatus();
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
setIsRefreshing(true);
try {
const storageStatus = authStorage.getStorageStatus();
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
const results = {
timestamp: new Date().toISOString(),
storage: storageStatus,
session: {
exists: !!session,
user: session?.user?.email || null,
expiresAt: session?.expires_at || null,
error: sessionError?.message || null,
},
network: {
online: navigator.onLine,
},
environment: {
url: window.location.href,
isIframe: window.self !== window.top,
cookiesEnabled: navigator.cookieEnabled,
}
};
setDiagnostics(results);
logger.debug('Auth diagnostics', { results });
const results = {
timestamp: new Date().toISOString(),
storage: storageStatus,
session: {
exists: !!session,
user: session?.user?.email || null,
expiresAt: session?.expires_at || null,
error: sessionError?.message || null,
},
network: {
online: navigator.onLine,
},
environment: {
url: window.location.href,
isIframe: window.self !== window.top,
cookiesEnabled: navigator.cookieEnabled,
}
};
setDiagnostics(results);
logger.debug('Auth diagnostics', { results });
} finally {
setIsRefreshing(false);
}
};
useEffect(() => {
@@ -119,7 +125,7 @@ export function AuthDiagnostics() {
Running in iframe - storage may be restricted
</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
</Button>
</CardContent>

View File

@@ -25,6 +25,7 @@ export function ConflictResolutionDialog({
onResolve,
}: ConflictResolutionDialogProps) {
const [resolutions, setResolutions] = useState<Record<string, string>>({});
const [isApplying, setIsApplying] = useState(false);
const { user } = useAuth();
const handleResolutionChange = (itemId: string, action: string) => {
@@ -44,6 +45,7 @@ export function ConflictResolutionDialog({
return;
}
setIsApplying(true);
const { resolveConflicts } = await import('@/lib/conflictResolutionService');
try {
@@ -67,6 +69,8 @@ export function ConflictResolutionDialog({
userId: user.id,
metadata: { conflictCount: conflicts.length }
});
} finally {
setIsApplying(false);
}
};
@@ -119,10 +123,10 @@ export function ConflictResolutionDialog({
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isApplying}>
Cancel
</Button>
<Button onClick={handleApply} disabled={!allConflictsResolved}>
<Button onClick={handleApply} loading={isApplying} loadingText="Applying..." disabled={!allConflictsResolved}>
Apply & Approve
</Button>
</DialogFooter>

View File

@@ -182,8 +182,8 @@ export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: A
>
Cancel
</Button>
<Button type="submit" disabled={submitting || !selectedRideId}>
{submitting ? 'Adding...' : 'Add Credit'}
<Button type="submit" loading={submitting} loadingText="Adding..." disabled={!selectedRideId}>
Add Credit
</Button>
</div>
</form>

View File

@@ -199,9 +199,9 @@ export function ReviewForm({
{/* 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" />
{submitting ? 'Submitting...' : 'Submit Review'}
Submit Review
</Button>
</form>
</CardContent>

View File

@@ -363,8 +363,7 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }:
<Button type="button" variant="outline" onClick={handleClose} disabled={loading}>
Cancel
</Button>
<Button type="submit" disabled={loading || !captchaToken}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<Button type="submit" loading={loading} loadingText="Changing Email..." disabled={!captchaToken}>
Change Email
</Button>
</DialogFooter>

View File

@@ -558,8 +558,8 @@ export function LocationTab() {
{/* Save Button */}
<div className="flex justify-end">
<Button type="submit" disabled={saving}>
{saving ? 'Saving...' : 'Save Settings'}
<Button type="submit" loading={saving} loadingText="Saving...">
Save Settings
</Button>
</div>
</form>

View File

@@ -493,8 +493,7 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
>
Back
</Button>
<Button onClick={verifyMFAAndUpdate} disabled={loading || totpCode.length !== 6}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<Button onClick={verifyMFAAndUpdate} loading={loading} loadingText="Verifying..." disabled={totpCode.length !== 6}>
Verify & Update
</Button>
</DialogFooter>

View File

@@ -450,8 +450,8 @@ export function PrivacyTab() {
{/* Save Button */}
<div className="flex justify-end">
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Privacy Settings'}
<Button type="submit" loading={loading} loadingText="Saving...">
Save Privacy Settings
</Button>
</div>
</form>