382 lines
27 KiB
TypeScript
382 lines
27 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { Head, useForm, usePage } from '@inertiajs/react';
|
|
import { PageProps } from '@/types';
|
|
import { swal } from '@/lib/swal';
|
|
import axios from 'axios';
|
|
|
|
// FilePond
|
|
import { FilePond, registerPlugin } from 'react-filepond';
|
|
import 'filepond/dist/filepond.min.css';
|
|
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
|
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
|
|
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
|
|
|
|
registerPlugin(FilePondPluginImagePreview, FilePondPluginFileValidateType);
|
|
|
|
interface SystemSettingsProps extends PageProps {
|
|
settings: Record<string, any>;
|
|
}
|
|
|
|
/* ─── Reusable Components ─────────────────── */
|
|
|
|
function SectionCard({ title, description, children, delay = '0s' }: { title: string; description: string; children: React.ReactNode; delay?: string }) {
|
|
return (
|
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden h-full flex flex-col anim-up" style={{ animationDelay: delay }}>
|
|
<div className="px-6 py-4 border-b border-gray-50 bg-gray-50/30">
|
|
<h2 className="text-sm font-bold text-[#3D4E4B] tracking-tight">{title}</h2>
|
|
<p className="text-xs text-gray-400 font-semibold tracking-tight mt-1">{description}</p>
|
|
</div>
|
|
<div className="p-8 flex-1">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToggleItem({ label, description, checked, onChange }: { label: string; description: string; checked: boolean; onChange: (v: boolean) => void }) {
|
|
return (
|
|
<div className="flex items-center justify-between py-3 border-b border-gray-50 last:border-0 hover:bg-gray-50/30 px-2 -mx-2 rounded-xl transition-colors group">
|
|
<div>
|
|
<div className="text-sm font-bold text-[#3D4E4B] tracking-tight group-hover:text-[#D4A017] transition-colors">{label}</div>
|
|
<div className="text-xs text-gray-400 font-medium">{description}</div>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" className="sr-only peer" checked={checked} onChange={e => onChange(e.target.checked)} />
|
|
<div className="w-10 h-5 bg-gray-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-400 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-[#D4A017] peer-checked:after:border-[#D4A017] border border-gray-200"></div>
|
|
</label>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InputField({ label, value, onChange, type = 'text', placeholder = '', error = '', required = false, maxLength, id }: any) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<label htmlFor={id} className="block text-xs font-semibold text-gray-500 tracking-tight ml-1">
|
|
{label} {required && <span className="text-red-500">*</span>}
|
|
</label>
|
|
<input
|
|
id={id} type={type} value={value} onChange={e => onChange(e.target.value)} placeholder={placeholder} maxLength={maxLength}
|
|
className={`input-field${error ? ' is-error' : ''}`}
|
|
/>
|
|
{error && <p className="text-xs text-red-500 font-semibold ml-1 mt-1">{error}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const SYSTEM_TABS = ['general', 'security', 'email', 'mobile'] as const;
|
|
type SystemTab = typeof SYSTEM_TABS[number];
|
|
|
|
function getSystemTabFromHash(): SystemTab {
|
|
const hash = window.location.hash.replace('#', '') as SystemTab;
|
|
return SYSTEM_TABS.includes(hash) ? hash : 'general';
|
|
}
|
|
|
|
export default function SystemSettings({ settings }: SystemSettingsProps) {
|
|
const [activeTab, setActiveTab] = useState<SystemTab>(getSystemTabFromHash);
|
|
|
|
useEffect(() => {
|
|
const onHashChange = () => setActiveTab(getSystemTabFromHash());
|
|
window.addEventListener('hashchange', onHashChange);
|
|
return () => window.removeEventListener('hashchange', onHashChange);
|
|
}, []);
|
|
|
|
const switchTab = (tab: SystemTab) => {
|
|
window.location.hash = tab;
|
|
setActiveTab(tab);
|
|
};
|
|
|
|
const { auth } = usePage<any>().props;
|
|
const [testRecipient, setTestRecipient] = useState(auth?.user?.email || '');
|
|
const [testingEmail, setTestingEmail] = useState(false);
|
|
|
|
const handleSendTestEmail = async () => {
|
|
if (!testRecipient) return;
|
|
setTestingEmail(true);
|
|
try {
|
|
const response = await axios.post(route('system.settings.test-email'), {
|
|
recipient: testRecipient,
|
|
mail_host: data.settings.mail_host,
|
|
mail_port: data.settings.mail_port,
|
|
mail_username: data.settings.mail_username,
|
|
mail_password: data.settings.mail_password,
|
|
mail_encryption: data.settings.mail_encryption,
|
|
mail_from_address: data.settings.mail_from_address,
|
|
mail_from_name: data.settings.mail_from_name,
|
|
});
|
|
|
|
if (response.data.success) {
|
|
swal.success('Success', response.data.message || 'Test email sent successfully!');
|
|
} else {
|
|
swal.error('SMTP Error', response.data.message || 'Failed to send test email.');
|
|
}
|
|
} catch (error: any) {
|
|
swal.error('Error', error.response?.data?.message || error.message || 'An error occurred.');
|
|
} finally {
|
|
setTestingEmail(false);
|
|
}
|
|
};
|
|
|
|
const { data, setData, post, processing, errors } = useForm({
|
|
settings: {
|
|
app_name: settings.app_name || '',
|
|
app_logo_text: settings.app_logo_text || 'B',
|
|
app_description: settings.app_description || '',
|
|
allow_registration: settings.allow_registration === '1' || settings.allow_registration === true,
|
|
require_email_verification: settings.require_email_verification === '1' || settings.require_email_verification === true,
|
|
password_minimum_length: parseInt(settings.password_minimum_length) || 8,
|
|
password_require_symbols: settings.password_require_symbols === '1' || settings.password_require_symbols === true,
|
|
password_require_numbers: settings.password_require_numbers === '1' || settings.password_require_numbers === true,
|
|
password_require_mixed_case: settings.password_require_mixed_case === '1' || settings.password_require_mixed_case === true,
|
|
two_factor_totp_enabled: settings.two_factor_totp_enabled === '1' || settings.two_factor_totp_enabled === true,
|
|
two_factor_email_enabled: settings.two_factor_email_enabled === '1' || settings.two_factor_email_enabled === true,
|
|
oauth_google_enabled: settings.oauth_google_enabled === '1' || settings.oauth_google_enabled === true,
|
|
oauth_google_client_id: settings.oauth_google_client_id || '',
|
|
oauth_google_client_secret: settings.oauth_google_client_secret || '',
|
|
oauth_github_enabled: settings.oauth_github_enabled === '1' || settings.oauth_github_enabled === true,
|
|
oauth_github_client_id: settings.oauth_github_client_id || '',
|
|
oauth_github_client_secret: settings.oauth_github_client_secret || '',
|
|
mail_host: settings.mail_host || '',
|
|
mail_port: settings.mail_port || '',
|
|
mail_username: settings.mail_username || '',
|
|
mail_password: settings.mail_password || '',
|
|
mail_encryption: settings.mail_encryption || 'tls',
|
|
mail_from_address: settings.mail_from_address || '',
|
|
mail_from_name: settings.mail_from_name || '',
|
|
primary_color: settings.primary_color || '#D4A017',
|
|
|
|
// Mobile App
|
|
android_latest_version: settings.android_latest_version || '1.0.0',
|
|
android_min_version: settings.android_min_version || '1.0.0',
|
|
android_maintenance_mode: settings.android_maintenance_mode === '1' || settings.android_maintenance_mode === true,
|
|
android_playstore_url: settings.android_playstore_url || '',
|
|
},
|
|
logo_file: null as File | null,
|
|
_method: 'PATCH',
|
|
});
|
|
|
|
const [files, setFiles] = useState<any[]>([]);
|
|
|
|
const handleChange = (key: string, value: any) => {
|
|
setData('settings', { ...data.settings, [key]: value });
|
|
};
|
|
|
|
const handleSave = (e: React.SyntheticEvent) => {
|
|
e.preventDefault();
|
|
post(route('system.settings.update'), {
|
|
preserveScroll: true,
|
|
onSuccess: () => swal.success('Saved', 'System settings updated successfully.'),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout>
|
|
<Head title="System Settings" />
|
|
|
|
{/* Premium Header Row */}
|
|
<div className="flex items-center justify-between mb-8 anim-down">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-[#3D4E4B] tracking-tight leading-none">System Configuration</h1>
|
|
<p className="text-sm font-semibold text-gray-400 tracking-tight mt-2">Manage global application behavior and external protocols</p>
|
|
</div>
|
|
<button onClick={handleSave} disabled={processing}
|
|
className="h-10 px-8 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all shadow-lg shadow-[#3D4E4B]/20 disabled:opacity-60 disabled:cursor-not-allowed">
|
|
{processing ? 'Saving...' : 'Save Configuration'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabbed Navigation Row */}
|
|
<div className="flex items-center gap-1 border-b border-gray-100 w-full mb-8 anim-down" style={{ animationDelay: '0.05s' }}>
|
|
<button type="button" onClick={() => switchTab('general')}
|
|
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === 'general' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
|
General & Branding
|
|
{activeTab === 'general' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
|
|
</button>
|
|
<button type="button" onClick={() => switchTab('security')}
|
|
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === 'security' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
|
Security & OAuth
|
|
{activeTab === 'security' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
|
|
</button>
|
|
<button type="button" onClick={() => switchTab('email')}
|
|
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === 'email' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
|
Email Protocol
|
|
{activeTab === 'email' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
|
|
</button>
|
|
<button type="button" onClick={() => switchTab('mobile')}
|
|
className={`relative pb-3 px-1 text-sm font-bold tracking-tight transition-colors ${activeTab === 'mobile' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
|
Mobile App Control
|
|
{activeTab === 'mobile' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div className="space-y-8 pb-20">
|
|
{activeTab === 'general' && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 anim-fade">
|
|
<SectionCard title="Application Branding" description="Logo and visual identity" delay="0.1s">
|
|
<div className="space-y-8">
|
|
<div className="flex flex-col sm:flex-row items-center gap-8">
|
|
<div className={`w-24 h-24 rounded-2xl flex items-center justify-center text-white text-3xl font-bold shrink-0 border border-gray-100 shadow-sm ${!settings.app_logo ? 'bg-[#3D4E4B]' : 'bg-white'}`}>
|
|
{settings.app_logo ? <img src={settings.app_logo} className="w-full h-full object-contain" /> : (data.settings.app_logo_text || 'B')}
|
|
</div>
|
|
<div className="flex-1 w-full">
|
|
<FilePond files={files} onupdatefiles={items => { setFiles(items); setData('logo_file', items[0]?.file as File || null); }}
|
|
allowMultiple={false} maxFiles={1} labelIdle='Drop Logo here' />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
<InputField label="App Name" value={data.settings.app_name} onChange={(v:any) => handleChange('app_name', v)} required placeholder="e.g. Biiskit Platform" />
|
|
<InputField label="Logo Text" value={data.settings.app_logo_text} onChange={(v:any) => handleChange('app_logo_text', v)} maxLength={3} required placeholder="e.g. B" />
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Public Policy" description="Manage open registration and access" delay="0.15s">
|
|
<div className="space-y-2">
|
|
<ToggleItem label="Public Registration" description="Allow new users to create accounts" checked={data.settings.allow_registration} onChange={v => handleChange('allow_registration', v)} />
|
|
<ToggleItem label="Email Verification" description="Require email validation for new accounts" checked={data.settings.require_email_verification} onChange={v => handleChange('require_email_verification', v)} />
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'security' && (
|
|
<div className="space-y-8 anim-fade">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<SectionCard title="Password Standards" description="Complexity requirements for user access" delay="0.1s">
|
|
<div className="space-y-2">
|
|
<ToggleItem label="Require Symbols" description="Include special characters (!@#$)" checked={data.settings.password_require_symbols} onChange={v => handleChange('password_require_symbols', v)} />
|
|
<ToggleItem label="Require Numbers" description="Include numerical digits (0-9)" checked={data.settings.password_require_numbers} onChange={v => handleChange('password_require_numbers', v)} />
|
|
<ToggleItem label="Require Mixed Case" description="Force Uppercase and Lowercase letters" checked={data.settings.password_require_mixed_case} onChange={v => handleChange('password_require_mixed_case', v)} />
|
|
<div className="flex items-center justify-between py-4 mt-2 border-t border-gray-50">
|
|
<span className="text-sm font-bold text-[#3D4E4B]">Minimum Length</span>
|
|
<input type="number" value={data.settings.password_minimum_length} onChange={e => handleChange('password_minimum_length', parseInt(e.target.value))} className="w-16 h-10 input-field text-center px-2 text-[#D4A017] font-bold" />
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Two-Factor Authentication (2FA)" description="Configure globally available 2FA options for users" delay="0.12s">
|
|
<div className="space-y-2">
|
|
<ToggleItem label="Google Authenticator (TOTP)" description="Allow users to use Authenticator Apps (Google, Authy, etc.)" checked={data.settings.two_factor_totp_enabled} onChange={v => handleChange('two_factor_totp_enabled', v)} />
|
|
<ToggleItem label="Email 2FA" description="Allow users to receive 6-digit OTP codes via email (requires SMTP configuration)" checked={data.settings.two_factor_email_enabled} onChange={v => handleChange('two_factor_email_enabled', v)} />
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<SectionCard title="OAuth Providers" description="Third-party authentication protocols" delay="0.15s">
|
|
<div className="space-y-6">
|
|
{['google', 'github'].map(prov => (
|
|
<div key={prov} className="p-6 rounded-2xl bg-gray-50/50 border border-gray-100">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="text-sm font-bold text-[#3D4E4B] tracking-tight capitalize flex items-center gap-2">
|
|
<span className={`w-2 h-2 rounded-full ${data.settings[`oauth_${prov}_enabled` as keyof typeof data.settings] ? 'bg-green-500' : 'bg-gray-300'}`} />
|
|
{prov} Login
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer scale-90">
|
|
<input type="checkbox" className="sr-only peer" checked={data.settings[`oauth_${prov}_enabled` as keyof typeof data.settings]} onChange={e => handleChange(`oauth_${prov}_enabled`, e.target.checked)} />
|
|
<div className="w-10 h-5 bg-gray-300 rounded-full peer peer-checked:bg-[#D4A017] after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-400 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full border border-gray-200"></div>
|
|
</label>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<InputField label="Client ID" value={data.settings[`oauth_${prov}_client_id` as keyof typeof data.settings]} onChange={(v:any) => handleChange(`oauth_${prov}_client_id`, v)} placeholder={`Enter ${prov} Client ID`} />
|
|
<InputField label="Client Secret" type="password" value={data.settings[`oauth_${prov}_client_secret` as keyof typeof data.settings]} onChange={(v:any) => handleChange(`oauth_${prov}_client_secret`, v)} placeholder="••••••••" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'email' && (
|
|
<div className="max-w-5xl space-y-8 anim-fade">
|
|
<SectionCard title="Mail Transmission" description="SMTP server configuration for system notifications" delay="0.1s">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
<div className="md:col-span-2 space-y-6">
|
|
<div className="grid grid-cols-3 gap-6">
|
|
<div className="col-span-2"><InputField label="Mail Host" value={data.settings.mail_host} onChange={(v:any) => handleChange('mail_host', v)} placeholder="e.g. smtp.mailtrap.io" required /></div>
|
|
<InputField label="Port" value={data.settings.mail_port} onChange={(v:any) => handleChange('mail_port', v)} placeholder="e.g. 587" required />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<InputField label="Username" value={data.settings.mail_username} onChange={(v:any) => handleChange('mail_username', v)} placeholder="e.g. user_key_123" />
|
|
<InputField label="Password" type="password" value={data.settings.mail_password} onChange={(v:any) => handleChange('mail_password', v)} placeholder="••••••••" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-6 pt-6 md:pt-0 border-t md:border-t-0 md:border-l border-gray-100 md:pl-8">
|
|
<InputField label="Sender Address" value={data.settings.mail_from_address} onChange={(v:any) => handleChange('mail_from_address', v)} placeholder="e.g. no-reply@app.com" required />
|
|
<InputField label="Sender Name" value={data.settings.mail_from_name} onChange={(v:any) => handleChange('mail_from_name', v)} placeholder="e.g. System Admin" required />
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="SMTP Connection Test" description="Verify your SMTP transmission parameters by sending a test email" delay="0.15s">
|
|
<div className="max-w-xl space-y-6">
|
|
<div className="p-4 rounded-xl bg-gray-50/50 border border-gray-100 flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-[#D4A017] mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<p className="text-xs text-[#3D4E4B] font-semibold leading-relaxed">
|
|
This test will use the SMTP values entered in the form above. You don't need to save the configuration first to test it.
|
|
</p>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 items-end">
|
|
<div className="sm:col-span-2">
|
|
<InputField
|
|
label="Recipient Email Address"
|
|
value={testRecipient}
|
|
onChange={setTestRecipient}
|
|
placeholder="e.g. you@example.com"
|
|
required
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleSendTestEmail}
|
|
disabled={testingEmail || !testRecipient}
|
|
className="h-10 px-6 bg-[#3D4E4B] text-white text-xs font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all shadow-md shadow-[#3D4E4B]/10 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{testingEmail ? (
|
|
<>
|
|
<svg className="animate-spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span>Transmitting...</span>
|
|
</>
|
|
) : 'Send Test Email'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'mobile' && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 anim-fade">
|
|
<SectionCard title="Version Management" description="Control Android application lifecycle" delay="0.1s">
|
|
<div className="space-y-8">
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<InputField label="Latest Version" value={data.settings.android_latest_version} onChange={(v:any) => handleChange('android_latest_version', v)} placeholder="e.g. 1.2.0" />
|
|
<InputField label="Min Required Version" value={data.settings.android_min_version} onChange={(v:any) => handleChange('android_min_version', v)} placeholder="e.g. 1.0.5" />
|
|
</div>
|
|
<InputField label="Play Store URL" value={data.settings.android_playstore_url} onChange={(v:any) => handleChange('android_playstore_url', v)} placeholder="https://play.google.com/store/apps/details?id=..." />
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Mobile Availability" description="Control API accessibility for devices" delay="0.15s">
|
|
<div className="space-y-4">
|
|
<ToggleItem label="Maintenance Mode" description="Block Android API access for maintenance" checked={data.settings.android_maintenance_mode} onChange={v => handleChange('android_maintenance_mode', v)} />
|
|
<div className="p-4 rounded-xl bg-amber-50 border border-amber-100 mt-4">
|
|
<p className="text-[10px] font-bold text-amber-800 leading-relaxed uppercase tracking-widest mb-1">Warning</p>
|
|
<p className="text-[11px] text-amber-700 font-medium">Enabling maintenance mode will return a 503 error to all mobile devices connecting to the API.</p>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|