From ca9b971c29a05102ab730ca942fc2828ab7fc90e Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sun, 22 Mar 2026 03:43:23 +0100 Subject: [PATCH] fix: improve Instance settings section spacing and save UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Section headings: text-lg with descriptions, dividers spaced my-10 - GeneralPanel: convert to auto-save (toggles save immediately, text fields save on blur) — no more hidden save bar - StreamingPanel: restore sticky save bar (complex multi-field form needs explicit save, and it's the only sticky bar now) --- .../instanceSettingsPanels/GeneralPanel.tsx | 142 +++++++----------- .../instanceSettingsPanels/StreamingPanel.tsx | 32 ++-- .../modals/settingsPanels/InstancePanel.tsx | 20 ++- 3 files changed, 87 insertions(+), 107 deletions(-) diff --git a/packages/web/src/components/modals/instanceSettingsPanels/GeneralPanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/GeneralPanel.tsx index 8cfd54a2..c9cf83d9 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/GeneralPanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/GeneralPanel.tsx @@ -1,75 +1,76 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useSettingsStore } from '../../../stores/settingsStore'; import { Toggle } from '../../ui/Toggle'; -import type { InstanceAdminSettings } from '@backspace/shared'; export function GeneralPanel() { const instanceSettings = useSettingsStore((s) => s.instanceSettings); const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings); - const [draft, setDraft] = useState(null); - const [saving, setSaving] = useState(false); + const [instanceName, setInstanceName] = useState(''); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const [saveError, setSaveError] = useState(''); - const [saveSuccess, setSaveSuccess] = useState(false); - const [gifKeyDirty, setGifKeyDirty] = useState(false); const [gifKeyDraft, setGifKeyDraft] = useState(''); + const [gifKeyDirty, setGifKeyDirty] = useState(false); useEffect(() => { if (instanceSettings) { - setDraft({ ...instanceSettings }); - // Don't populate the input with the masked value — show empty field + setInstanceName(instanceSettings.instanceName); setGifKeyDraft(''); setGifKeyDirty(false); } }, [instanceSettings]); - if (!draft) return
Loading settings...
; - - const baseChanges = instanceSettings && draft - ? draft.instanceName !== instanceSettings.instanceName || - draft.registrationOpen !== instanceSettings.registrationOpen || - draft.discoveryEnabled !== instanceSettings.discoveryEnabled - : false; - const hasChanges = baseChanges || gifKeyDirty; - - const handleSave = async () => { - setSaving(true); + const autoSave = useCallback(async (payload: Record) => { + setSaveStatus('saving'); setSaveError(''); - setSaveSuccess(false); try { - const payload: Partial = { - instanceName: draft!.instanceName, - registrationOpen: draft!.registrationOpen, - discoveryEnabled: draft!.discoveryEnabled, - }; - // Only include gifApiKey when the user actually modified it - if (gifKeyDirty) { - payload.gifApiKey = gifKeyDraft; - } await updateInstanceSettings(payload); - setGifKeyDirty(false); - setGifKeyDraft(''); - setSaveSuccess(true); - setTimeout(() => setSaveSuccess(false), 2000); + setSaveStatus('saved'); + setTimeout(() => setSaveStatus('idle'), 1500); } catch (err) { setSaveError(err instanceof Error ? err.message : 'Failed to save'); - } finally { - setSaving(false); + setSaveStatus('error'); + setTimeout(() => { setSaveStatus('idle'); setSaveError(''); }, 3000); + } + }, [updateInstanceSettings]); + + if (!instanceSettings) return
Loading settings...
; + + const handleToggle = (key: string, value: boolean) => { + autoSave({ [key]: value }); + }; + + const handleInstanceNameBlur = () => { + const trimmed = instanceName.trim(); + if (trimmed && trimmed !== instanceSettings.instanceName) { + autoSave({ instanceName: trimmed }); } }; - const handleReset = () => { - if (instanceSettings) setDraft({ ...instanceSettings }); - setGifKeyDirty(false); - setGifKeyDraft(''); - setSaveError(''); + const handleGifKeyBlur = () => { + if (gifKeyDirty) { + autoSave({ gifApiKey: gifKeyDraft }); + setGifKeyDirty(false); + setGifKeyDraft(''); + } + }; + + const handleClearGifKey = () => { + autoSave({ gifApiKey: '' }); }; return ( -
e.preventDefault()}> -
- Configure your Backspace instance. These settings affect all users. -
+
+ {/* Save status indicator */} + {saveStatus === 'saving' && ( +
Saving...
+ )} + {saveStatus === 'saved' && ( +
Saved
+ )} + {saveStatus === 'error' && ( +
{saveError}
+ )} {/* Instance Name */}
@@ -78,12 +79,13 @@ export function GeneralPanel() {
setDraft({ ...draft, instanceName: e.target.value.slice(0, 32) })} + value={instanceName} + onChange={(e) => setInstanceName(e.target.value.slice(0, 32))} + onBlur={handleInstanceNameBlur} placeholder="Backspace" className="input-standard w-full" /> -
{draft.instanceName.length}/32
+
{instanceName.length}/32
@@ -96,7 +98,7 @@ export function GeneralPanel() {
Open Registration
Allow new users to create accounts on this instance
- setDraft({ ...draft, registrationOpen: v })} /> + handleToggle('registrationOpen', v)} /> @@ -110,7 +112,7 @@ export function GeneralPanel() {
Space Discovery
Allow spaces to appear in the public Explore page
- setDraft({ ...draft, discoveryEnabled: v })} /> + handleToggle('discoveryEnabled', v)} /> @@ -126,19 +128,20 @@ export function GeneralPanel() { type="password" value={gifKeyDirty ? gifKeyDraft : ''} onChange={(e) => { setGifKeyDraft(e.target.value); setGifKeyDirty(true); }} - placeholder={draft.gifEnabled ? 'Key saved — enter new key to replace' : 'Klipy API key'} + onBlur={handleGifKeyBlur} + placeholder={instanceSettings.gifEnabled ? 'Key saved — enter new key to replace' : 'Klipy API key'} className="input-standard w-full" autoComplete="off" />
- {draft.gifEnabled ? 'Enabled' : 'Not configured'} + {instanceSettings.gifEnabled ? 'Enabled' : 'Not configured'} - {draft.gifEnabled && !gifKeyDirty && ( + {instanceSettings.gifEnabled && !gifKeyDirty && (
- - {/* Status messages */} - {saveError && ( -
{saveError}
- )} - {saveSuccess && ( -
Settings saved
- )} - - {/* Save / Reset bar */} - {hasChanges && ( -
-
- - -
-
- )} - + ); } diff --git a/packages/web/src/components/modals/instanceSettingsPanels/StreamingPanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/StreamingPanel.tsx index 4ecde221..fc44323c 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/StreamingPanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/StreamingPanel.tsx @@ -489,21 +489,23 @@ export function StreamingPanel() {
Settings saved
)} {hasChanges && ( -
-
- - +
+
+
+ + +
)} diff --git a/packages/web/src/components/modals/settingsPanels/InstancePanel.tsx b/packages/web/src/components/modals/settingsPanels/InstancePanel.tsx index 8e3dbb3a..62c8882f 100644 --- a/packages/web/src/components/modals/settingsPanels/InstancePanel.tsx +++ b/packages/web/src/components/modals/settingsPanels/InstancePanel.tsx @@ -25,35 +25,39 @@ export function InstancePanel() { }, [fetchInstanceSettings, fetchStreamingLimits]); return ( -
+
{/* General */} -

+

General

+

Configure your Backspace instance. These settings affect all users.

-
+
{/* Streaming */} -

+

Streaming

+

These limits apply to all users on this instance. Users can pick values within these bounds.

-
+
{/* Storage */} -

+

Storage

+

Monitor file storage usage and clean up orphaned files.

-
+
{/* Users */} -

+

Users

+

View and manage user accounts on this instance.

);