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 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<StorageStatus | null>(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 (
<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 (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '24px 16px 64px' }}>
<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.
</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' }}>
<Box size={22} />
<div style={{ flex: 1 }}>
@@ -82,9 +100,9 @@ export default function StorageSettingsPage() {
</button>
</div>
)}
</div>
</div>}
{status?.provider !== 'stuffbox' && (
{pageState === 'disconnected' && status && (
<div className="card" style={{ padding: '20px' }}>
<h2 style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}>
Connect Stuffbox
@@ -92,7 +110,13 @@ export default function StorageSettingsPage() {
<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.
</p>
<StorageConfigurationPrompt open onConfigured={loadStatus} onCancel={() => {}} variant="inline" />
<StorageConfigurationPrompt
open
stuffboxAvailable={status.stuffboxAvailable}
onConfigured={loadStatus}
onCancel={() => {}}
variant="inline"
/>
</div>
)}
{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>;
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<AbortController | null>(null);
const connectionPopupRef = useRef<Window | null>(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 () => {