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

View File

@@ -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 || '');
@@ -199,6 +200,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
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>

View File

@@ -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(() => {
@@ -263,6 +264,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
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>

View File

@@ -31,8 +31,11 @@ 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 () => {
setIsRefreshing(true);
try {
const storageStatus = authStorage.getStorageStatus(); const storageStatus = authStorage.getStorageStatus();
const { data: { session }, error: sessionError } = await supabase.auth.getSession(); const { data: { session }, error: sessionError } = await supabase.auth.getSession();
@@ -57,6 +60,9 @@ export function AuthDiagnostics() {
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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