Restyle Media Storage settings and render storage configuration inline
Hop-State: A_06FP83QXM2N0A8C2AXNC3X0 Hop-Proposal: R_06FP83QBS4MX9A1YANYTZ48 Hop-Task: T_06FP8311WS7RGCSMMDQFWB8 Hop-Attempt: AT_06FP8311WT376768DB2VXGR
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, Box, Cloud, HardDrive } from 'lucide-react';
|
||||
import { Box, Cloud, HardDrive } from 'lucide-react';
|
||||
import { ArrowLeftIcon } from '@/components/Icons';
|
||||
import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt';
|
||||
|
||||
interface StorageStatus {
|
||||
@@ -15,7 +16,6 @@ interface StorageStatus {
|
||||
export default function StorageSettingsPage() {
|
||||
const [status, setStatus] = useState<StorageStatus | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [showConnect, setShowConnect] = useState(false);
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
@@ -48,19 +48,25 @@ export default function StorageSettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<header style={{ padding: '16px', borderBottom: '1px solid var(--border)', position: 'sticky', top: 0, background: 'var(--background)', zIndex: 10 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Link href="/settings" className="btn btn-ghost btn-sm" aria-label="Back to settings"><ArrowLeft size={18} /></Link>
|
||||
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Media Storage</h1>
|
||||
</div>
|
||||
</header>
|
||||
<main style={{ maxWidth: '600px', margin: '0 auto', padding: '24px 16px 64px' }}>
|
||||
return (
|
||||
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '24px 16px 64px' }}>
|
||||
<header style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '32px' }}>
|
||||
<Link href="/settings" style={{ color: 'var(--foreground)' }} aria-label="Back to settings">
|
||||
<ArrowLeftIcon />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: 700 }}>Media Storage</h1>
|
||||
<p style={{ color: 'var(--foreground-tertiary)', fontSize: '14px' }}>
|
||||
Manage where your account stores uploaded media
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p style={{ color: 'var(--foreground-secondary)', lineHeight: 1.5, marginBottom: '20px' }}>
|
||||
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' }}>
|
||||
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
|
||||
{status?.provider === 'stuffbox' ? <Box size={22} /> : status?.provider === 's3' ? <Cloud size={22} /> : <HardDrive size={22} />}
|
||||
<div style={{ flex: 1 }}>
|
||||
@@ -69,13 +75,23 @@ export default function StorageSettingsPage() {
|
||||
{status?.s3Provider && status.provider === 's3' && <div style={{ color: 'var(--foreground-secondary)', fontSize: '13px', marginTop: '5px' }}>Provider: {status.s3Provider}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px', marginTop: '18px', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-primary" type="button" onClick={() => setShowConnect(true)} disabled={!status}>{status?.provider ? 'Change storage' : 'Connect storage'}</button>
|
||||
{status?.provider === 'stuffbox' && <button className="btn btn-ghost" type="button" onClick={disconnectStuffbox} disabled={isDisconnecting}>{isDisconnecting ? 'Disconnecting…' : 'Disconnect Stuffbox'}</button>}
|
||||
</div>
|
||||
{status?.provider === 'stuffbox' && (
|
||||
<button className="btn btn-ghost" type="button" onClick={disconnectStuffbox} disabled={isDisconnecting} style={{ marginTop: '18px' }}>
|
||||
{isDisconnecting ? 'Disconnecting…' : 'Disconnect Stuffbox'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: '20px' }}>
|
||||
<h2 style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}>
|
||||
{status?.provider ? 'Change storage' : 'Connect media storage'}
|
||||
</h2>
|
||||
<p style={{ color: 'var(--foreground-secondary)', fontSize: '14px', lineHeight: 1.5, marginBottom: '20px' }}>
|
||||
Choose Stuffbox for the simplest setup, or connect an S3-compatible bucket you already own.
|
||||
</p>
|
||||
<StorageConfigurationPrompt open onConfigured={loadStatus} onCancel={() => {}} variant="inline" />
|
||||
</div>
|
||||
{error && <p style={{ color: 'var(--error)', fontSize: '14px', marginTop: '14px' }}>{error}</p>}
|
||||
</main>
|
||||
<StorageConfigurationPrompt open={showConnect} onConfigured={async () => { setShowConnect(false); await loadStatus(); }} onCancel={() => setShowConnect(false)} />
|
||||
</>;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ interface StorageConfigurationPromptProps {
|
||||
open: boolean;
|
||||
onConfigured: () => void | Promise<void>;
|
||||
onCancel: () => void;
|
||||
variant?: 'modal' | 'inline';
|
||||
}
|
||||
|
||||
export function StorageConfigurationPrompt({ open, onConfigured, onCancel }: StorageConfigurationPromptProps) {
|
||||
export function StorageConfigurationPrompt({ open, onConfigured, onCancel, variant = 'modal' }: StorageConfigurationPromptProps) {
|
||||
const [provider, setProvider] = useState('r2');
|
||||
const [endpoint, setEndpoint] = useState('');
|
||||
const [publicBaseUrl, setPublicBaseUrl] = useState('');
|
||||
@@ -103,35 +104,38 @@ export function StorageConfigurationPrompt({ open, onConfigured, onCancel }: Sto
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 100000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px', background: 'rgba(0, 0, 0, 0.8)' }} onClick={onCancel}>
|
||||
<div className="card" style={{ width: '100%', maxWidth: '560px', maxHeight: '90vh', overflowY: 'auto', padding: '24px' }} onClick={(event) => event.stopPropagation()}>
|
||||
const content = (
|
||||
<>
|
||||
{variant === 'modal' && (
|
||||
<>
|
||||
<h3 style={{ fontSize: '20px', fontWeight: 600, marginBottom: '8px' }}>Connect media storage</h3>
|
||||
<p style={{ color: 'var(--foreground-secondary)', lineHeight: 1.5, marginBottom: '20px' }}>
|
||||
Your media lives outside this Synapsis node, so your account stays portable.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: '12px', padding: '18px', background: 'var(--background-secondary)' }}>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: '12px', padding: '18px', background: 'var(--background-secondary)' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start' }}>
|
||||
<Box size={24} style={{ flex: '0 0 auto', marginTop: '2px' }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '4px' }}>Stuffbox</div>
|
||||
<div style={{ color: 'var(--foreground-secondary)', fontSize: '14px', lineHeight: 1.45 }}>
|
||||
Connect once, then upload directly from your browser. Synapsis never sees your storage credentials.
|
||||
Connect once, then uploads just work. You own your media and can take it anywhere.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-primary" onClick={connectStuffbox} disabled={isSubmitting || isLoading || !stuffboxAvailable} style={{ width: '100%', marginTop: '16px' }}>
|
||||
{isSubmitting && !showS3 ? 'Connecting…' : stuffboxAvailable ? <>Connect Stuffbox <ExternalLink size={15} /></> : isLoading ? 'Loading…' : 'Stuffbox unavailable on this node'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className="btn btn-ghost" onClick={() => { setShowS3((value) => !value); setError(''); }} style={{ width: '100%', marginTop: '12px', justifyContent: 'space-between' }}>
|
||||
Use your own S3-compatible bucket <ChevronDown size={16} style={{ transform: showS3 ? 'rotate(180deg)' : undefined }} />
|
||||
</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => { setShowS3((value) => !value); setError(''); }} style={{ width: '100%', marginTop: '12px', justifyContent: 'space-between' }}>
|
||||
Use your own S3-compatible bucket <ChevronDown size={16} style={{ transform: showS3 ? 'rotate(180deg)' : undefined }} />
|
||||
</button>
|
||||
|
||||
{showS3 && (
|
||||
<form onSubmit={connectS3} style={{ marginTop: '16px' }}>
|
||||
{showS3 && (
|
||||
<form onSubmit={connectS3} style={{ marginTop: '16px' }}>
|
||||
<p style={{ color: 'var(--foreground-tertiary)', fontSize: '13px', lineHeight: 1.45, marginBottom: '14px' }}>
|
||||
Advanced option. Credentials are encrypted locally; you may need to confirm your password again after the node restarts.
|
||||
</p>
|
||||
@@ -153,11 +157,22 @@ export function StorageConfigurationPrompt({ open, onConfigured, onCancel }: Sto
|
||||
<Field label="Secret access key" value={secretKey} onChange={setSecretKey} type="password" minLength={10} />
|
||||
<Field label="Synapsis account password" value={password} onChange={setPassword} type="password" />
|
||||
<button type="submit" className="btn btn-primary" disabled={isSubmitting} style={{ width: '100%' }}>{isSubmitting ? 'Connecting…' : 'Connect S3 storage'}</button>
|
||||
</form>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
|
||||
{error && <div style={{ color: 'var(--error)', fontSize: '13px', marginTop: '14px' }}>{error}</div>}
|
||||
{error && <div style={{ color: 'var(--error)', fontSize: '13px', marginTop: '14px' }}>{error}</div>}
|
||||
{variant === 'modal' && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '16px' }}><button type="button" className="btn btn-ghost" onClick={onCancel} disabled={isSubmitting}>Cancel</button></div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === 'inline') return content;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 100000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px', background: 'rgba(0, 0, 0, 0.8)' }} onClick={onCancel}>
|
||||
<div className="card" style={{ width: '100%', maxWidth: '560px', maxHeight: '90vh', overflowY: 'auto', padding: '24px' }} onClick={(event) => event.stopPropagation()}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user