mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 08:51:13 -05:00
feat: Implement button loading states
This commit is contained in:
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user