feat(web): RegistrationPanel scaffold with both registration toggles

This commit is contained in:
Jannis Braun
2026-04-29 00:40:04 +02:00
parent 7edfd8ae26
commit cad6c50bff
2 changed files with 153 additions and 1 deletions
@@ -0,0 +1,149 @@
import { useEffect, useState } from 'react';
import { useSettingsStore } from '../../../stores/settingsStore';
import { useUIStore } from '../../../stores/uiStore';
import { Toggle } from '../../ui/Toggle';
interface RegistrationDraft {
registrationOpen: boolean;
federatedRegistrationOpen: boolean;
}
export function RegistrationPanel() {
const instanceSettings = useSettingsStore((s) => s.instanceSettings);
const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings);
const addToast = useUIStore((s) => s.addToast);
const [draft, setDraft] = useState<RegistrationDraft | null>(null);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState('');
useEffect(() => {
if (instanceSettings) {
setDraft({
registrationOpen: instanceSettings.registrationOpen,
federatedRegistrationOpen: instanceSettings.federatedRegistrationOpen,
});
setSaveError('');
}
}, [instanceSettings]);
if (!draft) return <div className="text-sm text-txt-tertiary">Loading settings...</div>;
const hasChanges = !!instanceSettings && (
draft.registrationOpen !== instanceSettings.registrationOpen ||
draft.federatedRegistrationOpen !== instanceSettings.federatedRegistrationOpen
);
const handleSave = async () => {
setSaving(true);
setSaveError('');
try {
await updateInstanceSettings({
registrationOpen: draft.registrationOpen,
federatedRegistrationOpen: draft.federatedRegistrationOpen,
});
addToast('Registration settings saved', 'success', 2000);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to save';
setSaveError(message);
addToast('Failed to update registration settings', 'warning');
} finally {
setSaving(false);
}
};
const handleReset = () => {
if (instanceSettings) {
setDraft({
registrationOpen: instanceSettings.registrationOpen,
federatedRegistrationOpen: instanceSettings.federatedRegistrationOpen,
});
}
setSaveError('');
};
return (
<form className="space-y-5" onSubmit={(e) => e.preventDefault()}>
<h2 className="text-lg font-semibold text-txt-primary">Registration</h2>
<div className="text-xs text-txt-tertiary">
Control who can create accounts on this instance. Public registration covers local
sign-ups; federated registration covers users from peered instances creating an account here.
</div>
{/* Public registration */}
<div>
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">Public Registration</div>
<div className="rounded-lg bg-white/[0.02] p-3.5">
<label className="flex items-center justify-between cursor-pointer gap-4">
<div className="flex-1">
<div className="text-sm font-medium text-txt-primary">Allow new local accounts</div>
<div className="text-xs text-txt-tertiary mt-0.5">
Anyone can create a local account from the registration page. When off, only invite
links can create new local accounts.
</div>
</div>
<Toggle
enabled={draft.registrationOpen}
onChange={(v) => setDraft({ ...draft, registrationOpen: v })}
/>
</label>
</div>
</div>
{/* Federated registration */}
<div>
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">Federated Registration</div>
<div className="rounded-lg bg-white/[0.02] p-3.5">
<label className="flex items-center justify-between cursor-pointer gap-4">
<div className="flex-1">
<div className="text-sm font-medium text-txt-primary">Allow new federated accounts</div>
<div className="text-xs text-txt-tertiary mt-0.5">
Users from other instances can create a federated account here via their Connections
settings. Existing federated accounts can always log in regardless of this setting.
</div>
</div>
<Toggle
enabled={draft.federatedRegistrationOpen}
onChange={(v) => setDraft({ ...draft, federatedRegistrationOpen: v })}
/>
</label>
</div>
</div>
{/* Invite Links — populated in subsequent tasks */}
<div className="border-t border-white/[0.06] pt-5">
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">Invite Links</div>
<div className="rounded-lg bg-white/[0.02] p-3.5 text-sm text-txt-tertiary">
Invite-link UI will appear here.
</div>
</div>
{/* Status messages */}
{saveError && (
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
)}
{/* Save / Reset bar */}
{hasChanges && (
<div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1">
<div className="glass-bubble rounded-full px-4 py-2 flex items-center gap-2 animate-slide-up pointer-events-auto">
<button
onClick={handleReset}
className="px-3 py-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Reset
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-full transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
)}
</form>
);
}
@@ -4,12 +4,13 @@ import { useSettingsSections } from '../../../hooks/useSettingsSections';
import type { SettingsSection } from '../SettingsSectionsContext'; import type { SettingsSection } from '../SettingsSectionsContext';
import { SettingsTabBar } from '../SettingsTabBar'; import { SettingsTabBar } from '../SettingsTabBar';
import { GeneralPanel } from '../instanceSettingsPanels/GeneralPanel'; import { GeneralPanel } from '../instanceSettingsPanels/GeneralPanel';
import { RegistrationPanel } from '../instanceSettingsPanels/RegistrationPanel';
import { FederationPanel } from '../instanceSettingsPanels/FederationPanel'; import { FederationPanel } from '../instanceSettingsPanels/FederationPanel';
import { StreamingPanel } from '../instanceSettingsPanels/StreamingPanel'; import { StreamingPanel } from '../instanceSettingsPanels/StreamingPanel';
import { StoragePanel } from '../instanceSettingsPanels/StoragePanel'; import { StoragePanel } from '../instanceSettingsPanels/StoragePanel';
import { UsersPanel } from '../instanceSettingsPanels/UsersPanel'; import { UsersPanel } from '../instanceSettingsPanels/UsersPanel';
type SubTab = 'general' | 'federation' | 'streaming' | 'storage' | 'users'; type SubTab = 'general' | 'registration' | 'federation' | 'streaming' | 'storage' | 'users';
export function InstancePanel() { export function InstancePanel() {
const fetchInstanceSettings = useSettingsStore((s) => s.fetchInstanceSettings); const fetchInstanceSettings = useSettingsStore((s) => s.fetchInstanceSettings);
@@ -20,6 +21,7 @@ export function InstancePanel() {
const sections = useMemo<SettingsSection[]>(() => [ const sections = useMemo<SettingsSection[]>(() => [
{ id: 'general', label: 'General' }, { id: 'general', label: 'General' },
{ id: 'registration', label: 'Registration' },
{ id: 'federation', label: 'Federation', badgeCount: approvalCount }, { id: 'federation', label: 'Federation', badgeCount: approvalCount },
{ id: 'streaming', label: 'Streaming' }, { id: 'streaming', label: 'Streaming' },
{ id: 'storage', label: 'Storage' }, { id: 'storage', label: 'Storage' },
@@ -43,6 +45,7 @@ export function InstancePanel() {
<SettingsTabBar /> <SettingsTabBar />
{subTab === 'general' && <GeneralPanel />} {subTab === 'general' && <GeneralPanel />}
{subTab === 'registration' && <RegistrationPanel />}
{subTab === 'federation' && <FederationPanel onApprovalCountChange={setApprovalCount} />} {subTab === 'federation' && <FederationPanel onApprovalCountChange={setApprovalCount} />}
{subTab === 'streaming' && <StreamingPanel />} {subTab === 'streaming' && <StreamingPanel />}
{subTab === 'storage' && <StoragePanel />} {subTab === 'storage' && <StoragePanel />}