feat(web): RegistrationPanel scaffold with both registration toggles
This commit is contained in:
@@ -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 />}
|
||||||
|
|||||||
Reference in New Issue
Block a user