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:
@@ -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';
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user