diff --git a/src/app/settings/storage/page.tsx b/src/app/settings/storage/page.tsx index 3cf5be0..53eb695 100644 --- a/src/app/settings/storage/page.tsx +++ b/src/app/settings/storage/page.tsx @@ -2,9 +2,10 @@ import { useCallback, useEffect, useState } from 'react'; import Link from 'next/link'; -import { Box, ExternalLink } from 'lucide-react'; +import { Box, ExternalLink, Loader2 } from 'lucide-react'; import { ArrowLeftIcon } from '@/components/Icons'; import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt'; +import { getStoragePageState } from './storage-page-state'; interface StorageStatus { provider: 'stuffbox' | null; @@ -15,9 +16,11 @@ interface StorageStatus { export default function StorageSettingsPage() { const [status, setStatus] = useState(null); const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(true); const [isDisconnecting, setIsDisconnecting] = useState(false); - const loadStatus = useCallback(async () => { + const loadStatus = useCallback(async (showLoading = false) => { + if (showLoading) setIsLoading(true); try { const response = await fetch('/api/storage/configuration', { cache: 'no-store' }); const data = await response.json().catch(() => ({})); @@ -26,10 +29,12 @@ export default function StorageSettingsPage() { setError(''); } catch (loadError) { setError(loadError instanceof Error ? loadError.message : 'Unable to load storage settings'); + } finally { + if (showLoading) setIsLoading(false); } }, []); - useEffect(() => { void loadStatus(); }, [loadStatus]); + useEffect(() => { void loadStatus(true); }, [loadStatus]); const disconnectStuffbox = async () => { if (!window.confirm('Disconnect Stuffbox from this Synapsis account? Existing media links will keep working.')) return; @@ -47,6 +52,19 @@ export default function StorageSettingsPage() { } }; + const pageState = getStoragePageState(status, isLoading); + + if (pageState === 'loading') { + return ( +
+
+
+
+ ); + } + return (
@@ -65,7 +83,7 @@ export default function StorageSettingsPage() { Storage belongs to your account rather than this node. That keeps your media available if you move your account elsewhere.

-
+ {status &&
@@ -82,9 +100,9 @@ export default function StorageSettingsPage() {
)} -
+
} - {status?.provider !== 'stuffbox' && ( + {pageState === 'disconnected' && status && (

Connect Stuffbox @@ -92,7 +110,13 @@ export default function StorageSettingsPage() {

Connect the official Synapsis storage partner to upload portable media from your account.

- {}} variant="inline" /> + {}} + variant="inline" + />

)} {error &&

{error}

} diff --git a/src/app/settings/storage/storage-page-state.test.ts b/src/app/settings/storage/storage-page-state.test.ts new file mode 100644 index 0000000..97760a4 --- /dev/null +++ b/src/app/settings/storage/storage-page-state.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { getStoragePageState } from './storage-page-state'; + +describe('storage page render gate', () => { + it('never treats missing initial status as a disconnected account', () => { + expect(getStoragePageState(null, true)).toBe('loading'); + }); + + it('renders the connection UI only after a disconnected status is loaded', () => { + expect(getStoragePageState({ provider: null }, false)).toBe('disconnected'); + expect(getStoragePageState({ provider: 'stuffbox' }, false)).toBe('connected'); + }); +}); diff --git a/src/app/settings/storage/storage-page-state.ts b/src/app/settings/storage/storage-page-state.ts new file mode 100644 index 0000000..9b6514c --- /dev/null +++ b/src/app/settings/storage/storage-page-state.ts @@ -0,0 +1,14 @@ +export interface StoragePageStatus { + provider: 'stuffbox' | null; +} + +export type StoragePageState = 'loading' | 'error' | 'connected' | 'disconnected'; + +export function getStoragePageState( + status: StoragePageStatus | null, + isLoading: boolean, +): StoragePageState { + if (isLoading) return 'loading'; + if (!status) return 'error'; + return status.provider === 'stuffbox' ? 'connected' : 'disconnected'; +} diff --git a/src/components/StorageConfigurationPrompt.tsx b/src/components/StorageConfigurationPrompt.tsx index c383b20..42b25b6 100644 --- a/src/components/StorageConfigurationPrompt.tsx +++ b/src/components/StorageConfigurationPrompt.tsx @@ -14,12 +14,13 @@ interface StorageConfigurationPromptProps { onConfigured: () => void | Promise; onCancel: () => void; variant?: 'modal' | 'inline'; + stuffboxAvailable?: boolean; } -export function StorageConfigurationPrompt({ open, onConfigured, onCancel, variant = 'modal' }: StorageConfigurationPromptProps) { +export function StorageConfigurationPrompt({ open, onConfigured, onCancel, variant = 'modal', stuffboxAvailable: prefetchedStuffboxAvailable }: StorageConfigurationPromptProps) { const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [stuffboxAvailable, setStuffboxAvailable] = useState(false); + const [stuffboxAvailable, setStuffboxAvailable] = useState(prefetchedStuffboxAvailable ?? false); const [isConnectingStuffbox, setIsConnectingStuffbox] = useState(false); const connectionAbortRef = useRef(null); const connectionPopupRef = useRef(null); @@ -51,6 +52,12 @@ export function StorageConfigurationPrompt({ open, onConfigured, onCancel, varia useEffect(() => { if (!open) return; + if (prefetchedStuffboxAvailable !== undefined) { + setError(''); + setStuffboxAvailable(prefetchedStuffboxAvailable); + setIsLoading(false); + return; + } let active = true; setError(''); setIsLoading(true); @@ -63,7 +70,7 @@ export function StorageConfigurationPrompt({ open, onConfigured, onCancel, varia .catch((loadError) => active && setError(loadError instanceof Error ? loadError.message : 'Unable to load storage options')) .finally(() => active && setIsLoading(false)); return () => { active = false; }; - }, [open]); + }, [open, prefetchedStuffboxAvailable]); if (!open) return null; const connectStuffbox = async () => {