feat: inisialisasi project kit v2
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
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,
|
||||
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="grid grid-cols-1 lg:grid-cols-2 gap-8 anim-fade">
|
||||
<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="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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user