Gate storage settings rendering until account storage status is ready

Hop-State: A_06FPCTGM3Q2ZPDN6C6BPVQR
Hop-Proposal: R_06FPCTEQFP5N8419897F3QG
Hop-Task: T_06FPCSTHZSG7HGC71RC8FNR
Hop-Attempt: AT_06FPCSTHZRZPFGE0XP75VKR
This commit is contained in:
2026-07-15 08:34:15 -07:00
committed by Hop
parent 0879b7b60e
commit 5b697e3f9e
4 changed files with 69 additions and 10 deletions
+31 -7
View File
@@ -2,9 +2,10 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Box, ExternalLink } from 'lucide-react'; import { Box, ExternalLink, Loader2 } from 'lucide-react';
import { ArrowLeftIcon } from '@/components/Icons'; import { ArrowLeftIcon } from '@/components/Icons';
import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt'; import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt';
import { getStoragePageState } from './storage-page-state';
interface StorageStatus { interface StorageStatus {
provider: 'stuffbox' | null; provider: 'stuffbox' | null;
@@ -15,9 +16,11 @@ interface StorageStatus {
export default function StorageSettingsPage() { export default function StorageSettingsPage() {
const [status, setStatus] = useState<StorageStatus | null>(null); const [status, setStatus] = useState<StorageStatus | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isDisconnecting, setIsDisconnecting] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false);
const loadStatus = useCallback(async () => { const loadStatus = useCallback(async (showLoading = false) => {
if (showLoading) setIsLoading(true);
try { try {
const response = await fetch('/api/storage/configuration', { cache: 'no-store' }); const response = await fetch('/api/storage/configuration', { cache: 'no-store' });
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
@@ -26,10 +29,12 @@ export default function StorageSettingsPage() {
setError(''); setError('');
} catch (loadError) { } catch (loadError) {
setError(loadError instanceof Error ? loadError.message : 'Unable to load storage settings'); 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 () => { const disconnectStuffbox = async () => {
if (!window.confirm('Disconnect Stuffbox from this Synapsis account? Existing media links will keep working.')) return; 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 (
<main aria-busy="true" aria-label="Loading media storage" style={{ minHeight: '70vh', display: 'grid', placeItems: 'center', padding: '24px' }}>
<div role="status" style={{ display: 'flex', alignItems: 'center', gap: '10px', color: 'var(--foreground-secondary)' }}>
<Loader2 className="animate-spin" size={22} aria-hidden="true" />
<span>Loading media storage</span>
</div>
</main>
);
}
return ( return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '24px 16px 64px' }}> <div style={{ maxWidth: '600px', margin: '0 auto', padding: '24px 16px 64px' }}>
<header style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '32px' }}> <header style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '32px' }}>
@@ -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. Storage belongs to your account rather than this node. That keeps your media available if you move your account elsewhere.
</p> </p>
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}> {status && <div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}> <div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
<Box size={22} /> <Box size={22} />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
@@ -82,9 +100,9 @@ export default function StorageSettingsPage() {
</button> </button>
</div> </div>
)} )}
</div> </div>}
{status?.provider !== 'stuffbox' && ( {pageState === 'disconnected' && status && (
<div className="card" style={{ padding: '20px' }}> <div className="card" style={{ padding: '20px' }}>
<h2 style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}> <h2 style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}>
Connect Stuffbox Connect Stuffbox
@@ -92,7 +110,13 @@ export default function StorageSettingsPage() {
<p style={{ color: 'var(--foreground-secondary)', fontSize: '14px', lineHeight: 1.5, marginBottom: '20px' }}> <p style={{ color: 'var(--foreground-secondary)', fontSize: '14px', lineHeight: 1.5, marginBottom: '20px' }}>
Connect the official Synapsis storage partner to upload portable media from your account. Connect the official Synapsis storage partner to upload portable media from your account.
</p> </p>
<StorageConfigurationPrompt open onConfigured={loadStatus} onCancel={() => {}} variant="inline" /> <StorageConfigurationPrompt
open
stuffboxAvailable={status.stuffboxAvailable}
onConfigured={loadStatus}
onCancel={() => {}}
variant="inline"
/>
</div> </div>
)} )}
{error && <p style={{ color: 'var(--error)', fontSize: '14px', marginTop: '14px' }}>{error}</p>} {error && <p style={{ color: 'var(--error)', fontSize: '14px', marginTop: '14px' }}>{error}</p>}
@@ -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');
});
});
@@ -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';
}
+10 -3
View File
@@ -14,12 +14,13 @@ interface StorageConfigurationPromptProps {
onConfigured: () => void | Promise<void>; onConfigured: () => void | Promise<void>;
onCancel: () => void; onCancel: () => void;
variant?: 'modal' | 'inline'; 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 [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [stuffboxAvailable, setStuffboxAvailable] = useState(false); const [stuffboxAvailable, setStuffboxAvailable] = useState(prefetchedStuffboxAvailable ?? false);
const [isConnectingStuffbox, setIsConnectingStuffbox] = useState(false); const [isConnectingStuffbox, setIsConnectingStuffbox] = useState(false);
const connectionAbortRef = useRef<AbortController | null>(null); const connectionAbortRef = useRef<AbortController | null>(null);
const connectionPopupRef = useRef<Window | null>(null); const connectionPopupRef = useRef<Window | null>(null);
@@ -51,6 +52,12 @@ export function StorageConfigurationPrompt({ open, onConfigured, onCancel, varia
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
if (prefetchedStuffboxAvailable !== undefined) {
setError('');
setStuffboxAvailable(prefetchedStuffboxAvailable);
setIsLoading(false);
return;
}
let active = true; let active = true;
setError(''); setError('');
setIsLoading(true); 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')) .catch((loadError) => active && setError(loadError instanceof Error ? loadError.message : 'Unable to load storage options'))
.finally(() => active && setIsLoading(false)); .finally(() => active && setIsLoading(false));
return () => { active = false; }; return () => { active = false; };
}, [open]); }, [open, prefetchedStuffboxAvailable]);
if (!open) return null; if (!open) return null;
const connectStuffbox = async () => { const connectStuffbox = async () => {