Fix Stuffbox popup completion and preflight storage before file selection

Hop-State: A_06FP87Y83XVM00YY2RB0X2G
Hop-Proposal: R_06FP87XN1TX5B0CW1ZFDEK8
Hop-Task: T_06FP86NZGXSMEX63FZDAS70
Hop-Attempt: AT_06FP86NZGYZEGPH937G3WK8
This commit is contained in:
2026-07-14 21:53:52 -07:00
committed by Hop
parent 270d5437f0
commit 67b96d6725
9 changed files with 224 additions and 70 deletions
+38 -13
View File
@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import AutoTextarea from '@/components/AutoTextarea';
import { StorageSessionPrompt } from '@/components/StorageSessionPrompt';
@@ -8,7 +8,7 @@ import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPro
import { useToast } from '@/lib/contexts/ToastContext';
import { useAccentColor } from '@/lib/contexts/AccentColorContext';
import { refreshStorageSession } from '@/lib/storage/client';
import { MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
export default function AdminPage() {
const { showToast } = useToast();
@@ -30,6 +30,7 @@ export default function AdminPage() {
});
const [savingSettings, setSavingSettings] = useState(false);
const [isUploadingBanner, setIsUploadingBanner] = useState(false);
const [isCheckingBannerStorage, setIsCheckingBannerStorage] = useState(false);
const [bannerUploadError, setBannerUploadError] = useState<string | null>(null);
const [showBannerSessionPrompt, setShowBannerSessionPrompt] = useState(false);
const [showBannerStorageConfiguration, setShowBannerStorageConfiguration] = useState(false);
@@ -41,6 +42,7 @@ export default function AdminPage() {
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null);
const bannerInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetch('/api/admin/me')
.then((res) => res.json())
@@ -145,6 +147,23 @@ export default function AdminPage() {
await uploadBannerFile(file);
};
const handleChooseBanner = async () => {
setBannerUploadError(null);
setIsCheckingBannerStorage(true);
try {
const provider = await getStorageProvider();
if (!provider) {
setShowBannerStorageConfiguration(true);
return;
}
bannerInputRef.current?.click();
} catch (error) {
setBannerUploadError(error instanceof Error ? error.message : 'Unable to check media storage');
} finally {
setIsCheckingBannerStorage(false);
}
};
const handleBannerSessionSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -418,16 +437,17 @@ export default function AdminPage() {
<div>
<label style={{ fontSize: '13px', fontWeight: 500, marginBottom: '4px', display: 'block' }}>Banner image</label>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
<label className="btn btn-ghost btn-sm">
{isUploadingBanner ? 'Uploading...' : 'Upload banner'}
<input
type="file"
accept="image/*"
onChange={handleBannerUpload}
disabled={isUploadingBanner}
style={{ display: 'none' }}
/>
</label>
<button className="btn btn-ghost btn-sm" type="button" onClick={handleChooseBanner} disabled={isUploadingBanner || isCheckingBannerStorage}>
{isUploadingBanner ? 'Uploading...' : isCheckingBannerStorage ? 'Checking storage…' : 'Upload banner'}
</button>
<input
ref={bannerInputRef}
type="file"
accept="image/*"
onChange={handleBannerUpload}
disabled={isUploadingBanner}
style={{ display: 'none' }}
/>
{bannerUploadError && (
<span style={{ fontSize: '12px', color: 'var(--danger)' }}>{bannerUploadError}</span>
)}
@@ -608,7 +628,12 @@ export default function AdminPage() {
open={showBannerStorageConfiguration}
onConfigured={async () => {
setShowBannerStorageConfiguration(false);
if (pendingBannerFile) await uploadBannerFile(pendingBannerFile, false);
if (pendingBannerFile) {
await uploadBannerFile(pendingBannerFile, false);
return;
}
showToast('Stuffbox connected. Choose a banner to continue.', 'success');
bannerInputRef.current?.click();
}}
onCancel={() => {
setShowBannerStorageConfiguration(false);
+2 -12
View File
@@ -3,20 +3,10 @@ import { requireAuth } from '@/lib/auth';
import { exchangeAuthorizationCode, StuffboxApiError } from '@/lib/stuffbox/client';
import { consumeStuffboxConnectionState } from '@/lib/stuffbox/connection-state';
import { saveStuffboxTokens } from '@/lib/stuffbox/tokens';
import { renderStuffboxPopupResponse } from '@/lib/stuffbox/popup-response';
function popupResponse(origin: string, success: boolean, message: string): NextResponse {
const safeJson = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c');
const payload = safeJson({ type: 'synapsis:stuffbox', success, message });
const targetOrigin = safeJson(origin);
const fallback = success ? '/settings/storage?stuffbox=connected' : '/settings/storage?stuffbox=error';
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Stuffbox</title></head><body>
<p>${success ? 'Stuffbox connected. You can close this window.' : 'Stuffbox could not be connected.'}</p>
<script>
if (window.opener) { window.opener.postMessage(${payload}, ${targetOrigin}); window.close(); }
else { window.location.replace(${safeJson(fallback)}); }
</script>
</body></html>`;
return new NextResponse(html, {
return new NextResponse(renderStuffboxPopupResponse(origin, success, message), {
status: success ? 200 : 400,
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
});
+4 -4
View File
@@ -717,6 +717,9 @@ a.btn-primary:visited {
color: var(--foreground-secondary);
cursor: pointer;
transition: all 0.15s ease;
border: none;
padding: 0;
background: transparent;
}
.compose-media-button:hover {
@@ -725,10 +728,7 @@ a.btn-primary:visited {
}
.compose-media-input {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
display: none;
}
.compose-media-error {