feat: inisialisasi project kit v2
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, usePage, useForm, router } from '@inertiajs/react';
|
||||
import { PageProps } from '@/types';
|
||||
import Swal from 'sweetalert2';
|
||||
import { swal } from '@/lib/swal';
|
||||
|
||||
// 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 SettingsProps extends PageProps {
|
||||
mustVerifyEmail: boolean;
|
||||
status?: string;
|
||||
twoFactor: {
|
||||
enabled: boolean;
|
||||
qr_code: string | null;
|
||||
secret: string | null;
|
||||
recovery_codes: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/* ─── Reusable Components from System Settings ─────────────────── */
|
||||
|
||||
function SectionCard({ title, description, children, delay = '0s', variant = 'default' }: { title: string; description: string; children: React.ReactNode; delay?: string; variant?: 'default' | 'danger' }) {
|
||||
const isDanger = variant === 'danger';
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-2xl border ${isDanger ? 'border-red-200 shadow-none' : '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 ${isDanger ? 'border-red-100 bg-red-50' : 'border-gray-50 bg-gray-50/30'}`}>
|
||||
<h2 className={`text-sm font-bold tracking-tight ${isDanger ? 'text-red-600' : 'text-[#3D4E4B]'}`}>{title}</h2>
|
||||
<p className={`text-xs font-semibold tracking-tight mt-1 ${isDanger ? 'text-red-400' : 'text-gray-400'}`}>{description}</p>
|
||||
</div>
|
||||
<div className="p-8 flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputField({ label, id, type = 'text', value, onChange, error, placeholder, required = false }: 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={onChange} placeholder={placeholder}
|
||||
className={`input-field${error ? ' is-error' : ''}`}
|
||||
/>
|
||||
{error && <p className="text-xs text-red-500 font-semibold ml-1 mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SETTINGS_TABS = ['profile', 'security', '2fa', 'danger'] as const;
|
||||
type SettingsTab = typeof SETTINGS_TABS[number];
|
||||
|
||||
function getSettingsTabFromHash(): SettingsTab {
|
||||
const hash = window.location.hash.replace('#', '') as SettingsTab;
|
||||
return SETTINGS_TABS.includes(hash) ? hash : 'profile';
|
||||
}
|
||||
|
||||
export default function SettingsIndex({ twoFactor }: SettingsProps) {
|
||||
const { user } = usePage<PageProps>().props.auth;
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>(getSettingsTabFromHash);
|
||||
|
||||
useEffect(() => {
|
||||
const onHashChange = () => setActiveTab(getSettingsTabFromHash());
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
return () => window.removeEventListener('hashchange', onHashChange);
|
||||
}, []);
|
||||
|
||||
const switchTab = (tab: SettingsTab) => {
|
||||
window.location.hash = tab;
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const { data: profileData, setData: setProfileData, post: postProfile, processing: profileProcessing, errors: profileErrors } = useForm({
|
||||
first_name: user.first_name || '',
|
||||
last_name: user.last_name || '',
|
||||
email: user.email || '',
|
||||
phone: (user as any).phone || '',
|
||||
bio: (user as any).bio || '',
|
||||
avatar_file: null as File | null,
|
||||
_method: 'PATCH',
|
||||
});
|
||||
|
||||
const [avatarFiles, setAvatarFiles] = useState<any[]>([]);
|
||||
|
||||
const handleProfileSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
postProfile(route('profile.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => swal.success('Saved', 'Profile updated successfully.'),
|
||||
});
|
||||
};
|
||||
|
||||
const { data: passwordData, setData: setPasswordData, put: putPassword, processing: passwordProcessing, errors: passwordErrors, reset: resetPassword } = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const handlePasswordSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
putPassword(route('password.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
resetPassword();
|
||||
swal.success('Saved', 'Password updated successfully.');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
const result = await swal.confirm('Delete account?', 'This action is irreversible.', 'Delete Permanently');
|
||||
if (result.isConfirmed) {
|
||||
const { value: password } = await Swal.fire({
|
||||
title: 'Confirm Password',
|
||||
input: 'password',
|
||||
inputPlaceholder: 'Enter your current password',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Delete',
|
||||
confirmButtonColor: '#dc2626',
|
||||
});
|
||||
if (password) {
|
||||
router.delete(route('profile.destroy'), {
|
||||
data: { password }, preserveScroll: true,
|
||||
onSuccess: () => swal.success('Deleted', 'Account removed.'),
|
||||
onError: (errs) => swal.error('Error', errs.password || 'Incorrect password.'),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initials = `${user.first_name?.charAt(0) || ''}${user.last_name?.charAt(0) || ''}`.toUpperCase();
|
||||
|
||||
// 2FA
|
||||
const [copiedSecret, setCopiedSecret] = useState(false);
|
||||
const [showCodes, setShowCodes] = useState(false);
|
||||
const twoFactorForm = useForm({ code: '' });
|
||||
|
||||
const copySecret = () => {
|
||||
navigator.clipboard.writeText(twoFactor.secret || '');
|
||||
setCopiedSecret(true);
|
||||
setTimeout(() => setCopiedSecret(false), 2000);
|
||||
};
|
||||
|
||||
const handleEnable2FA = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
twoFactorForm.post(route('two-factor.enable'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => { twoFactorForm.reset(); swal.success('Enabled', '2FA is now active on your account.'); },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisable2FA = async () => {
|
||||
const { value: password } = await Swal.fire({
|
||||
title: 'Disable 2FA',
|
||||
text: 'Enter your password to confirm.',
|
||||
input: 'password',
|
||||
inputPlaceholder: 'Your current password',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Disable',
|
||||
confirmButtonColor: '#dc2626',
|
||||
});
|
||||
if (password) {
|
||||
router.post(route('two-factor.disable'), { password }, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => swal.success('Disabled', '2FA has been disabled.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
const result = await swal.confirm('Regenerate Codes?', 'Old recovery codes will be invalidated.', 'Regenerate');
|
||||
if (result.isConfirmed) {
|
||||
router.post(route('two-factor.recovery-codes'), {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => { setShowCodes(true); swal.success('Regenerated', 'New recovery codes generated.'); },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<Head title="Account Settings" />
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between mb-8 anim-down">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-[#3D4E4B] tracking-tight leading-none">Account Settings</h1>
|
||||
<p className="text-sm font-semibold text-gray-400 tracking-tight mt-2">Manage your personal credentials and security preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs 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('profile')}
|
||||
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === 'profile' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
||||
Profile Information
|
||||
{activeTab === 'profile' && <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 & Password
|
||||
{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('2fa')}
|
||||
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === '2fa' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
||||
Two-Factor Auth
|
||||
{activeTab === '2fa' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
|
||||
</button>
|
||||
<button type="button" onClick={() => switchTab('danger')}
|
||||
className={`relative pb-3 px-1 text-sm font-bold tracking-tight transition-colors ${activeTab === 'danger' ? 'text-red-600' : 'text-gray-400 hover:text-red-600'}`}>
|
||||
Danger Zone
|
||||
{activeTab === 'danger' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-red-500 rounded-t-full" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="space-y-8 pb-20">
|
||||
{activeTab === 'profile' && (
|
||||
<div className="space-y-8 anim-fade">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2">
|
||||
<SectionCard title="Identity" description="Manage your public name and contact email" delay="0.1s">
|
||||
<form onSubmit={handleProfileSubmit} className="space-y-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<InputField label="First Name" id="first_name" value={profileData.first_name}
|
||||
onChange={(e: any) => setProfileData('first_name', e.target.value)}
|
||||
error={profileErrors.first_name} required placeholder="e.g. John" />
|
||||
<InputField label="Last Name" id="last_name" value={profileData.last_name}
|
||||
onChange={(e: any) => setProfileData('last_name', e.target.value)}
|
||||
error={profileErrors.last_name} required placeholder="e.g. Doe" />
|
||||
</div>
|
||||
<InputField label="Email Address" id="email" type="email" value={profileData.email}
|
||||
onChange={(e: any) => setProfileData('email', e.target.value)}
|
||||
error={profileErrors.email} required placeholder="e.g. john.doe@example.com" />
|
||||
<InputField label="Phone Number" id="phone" value={profileData.phone}
|
||||
onChange={(e: any) => setProfileData('phone', e.target.value)}
|
||||
error={(profileErrors as any).phone} placeholder="e.g. +62 812 3456 7890" />
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="bio" className="block text-xs font-semibold text-gray-500 tracking-tight ml-1">Bio</label>
|
||||
<textarea
|
||||
id="bio"
|
||||
value={profileData.bio}
|
||||
onChange={(e: any) => setProfileData('bio', e.target.value)}
|
||||
rows={3}
|
||||
placeholder="A short description about yourself..."
|
||||
className={`input-field py-3 resize-none${(profileErrors as any).bio ? ' is-error' : ''}`}
|
||||
/>
|
||||
{(profileErrors as any).bio && <p className="text-xs text-red-500 font-semibold ml-1 mt-1">{(profileErrors as any).bio}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-gray-50">
|
||||
<button type="submit" disabled={profileProcessing}
|
||||
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">
|
||||
{profileProcessing ? 'Saving...' : 'Save Profile'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<SectionCard title="Profile Photo" description="Update your avatar" delay="0.15s">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="w-24 h-24 rounded-2xl bg-[#3D4E4B] flex items-center justify-center text-white text-2xl font-bold shadow-lg shadow-[#3D4E4B]/10 overflow-hidden border-2 border-white">
|
||||
{user.avatar_url ? <img src={user.avatar_url} className="w-full h-full object-cover" /> : initials}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<FilePond files={avatarFiles} onupdatefiles={items => { setAvatarFiles(items); setProfileData('avatar_file', items[0]?.file as File || null); }}
|
||||
allowMultiple={false} maxFiles={1} labelIdle='Drop Photo here' />
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<div className="max-w-4xl anim-fade">
|
||||
<SectionCard title="Password" description="Update your secure access key" delay="0.1s">
|
||||
<form onSubmit={handlePasswordSubmit} className="space-y-8">
|
||||
<div className="max-w-md">
|
||||
<InputField label="Current Password" id="current_password" type="password"
|
||||
value={passwordData.current_password}
|
||||
onChange={(e: any) => setPasswordData('current_password', e.target.value)}
|
||||
error={passwordErrors.current_password} required placeholder="••••••••" />
|
||||
</div>
|
||||
<div className="h-px bg-gray-50 -mx-8" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
|
||||
<InputField label="New Password" id="password" type="password"
|
||||
value={passwordData.password}
|
||||
onChange={(e: any) => setPasswordData('password', e.target.value)}
|
||||
error={passwordErrors.password} required placeholder="Enter new password" />
|
||||
<InputField label="Confirm New Password" id="password_confirmation" type="password"
|
||||
value={passwordData.password_confirmation}
|
||||
onChange={(e: any) => setPasswordData('password_confirmation', e.target.value)}
|
||||
error={(passwordErrors as any).password_confirmation} required placeholder="Repeat new password" />
|
||||
</div>
|
||||
<div className="flex justify-end pt-4 border-t border-gray-50">
|
||||
<button type="submit" disabled={passwordProcessing}
|
||||
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">
|
||||
{passwordProcessing ? 'Updating...' : 'Update Password'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</SectionCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === '2fa' && (
|
||||
<div className="max-w-3xl space-y-6 anim-fade">
|
||||
<div className="p-5 bg-amber-50 border border-amber-200 rounded-2xl flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-amber-800 mb-1">Two-Factor Authentication (2FA)</p>
|
||||
<p className="text-[11px] text-amber-700 font-medium leading-relaxed">
|
||||
2FA menambah lapisan keamanan ekstra dengan meminta kode OTP dari aplikasi authenticator setiap kali login.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!twoFactor.enabled ? (
|
||||
<SectionCard title="Setup Authenticator App" description="Scan QR code dengan Google Authenticator, Authy, atau TOTP app lainnya">
|
||||
<div className="flex flex-col md:flex-row gap-10 items-center md:items-start">
|
||||
<div className="shrink-0">
|
||||
<div className="p-4 bg-white border-2 border-gray-100 rounded-2xl inline-block shadow-sm">
|
||||
{twoFactor.qr_code && <img src={twoFactor.qr_code} alt="2FA QR Code" className="w-44 h-44" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
'Install aplikasi authenticator (Google Authenticator, Authy, 1Password)',
|
||||
'Scan QR code atau masukkan manual key di bawah',
|
||||
'Masukkan kode 6 digit dari aplikasi untuk mengaktifkan',
|
||||
].map((step, i) => (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-[#3D4E4B] text-white text-[10px] font-black flex items-center justify-center shrink-0 mt-0.5">{i + 1}</div>
|
||||
<p className="text-xs font-medium text-gray-500 leading-relaxed pt-0.5">{step}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Manual Key</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-xs font-mono bg-gray-50 border border-gray-100 rounded-xl px-4 py-3 text-[#3D4E4B] tracking-wider">
|
||||
{twoFactor.secret}
|
||||
</code>
|
||||
<button onClick={copySecret} type="button"
|
||||
className="shrink-0 h-11 px-4 border border-gray-200 rounded-xl text-xs font-bold text-gray-500 hover:bg-gray-50 transition-all">
|
||||
{copiedSecret ? '✓ Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleEnable2FA} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Verification Code *</label>
|
||||
<input type="text" inputMode="numeric" maxLength={6}
|
||||
value={twoFactorForm.data.code}
|
||||
onChange={e => twoFactorForm.setData('code', e.target.value)}
|
||||
className="input-field w-full text-center tracking-[0.5em] font-bold text-lg"
|
||||
placeholder="000000" />
|
||||
{twoFactorForm.errors.code && <p className="text-xs text-red-500 font-semibold mt-1">{twoFactorForm.errors.code}</p>}
|
||||
</div>
|
||||
<button type="submit" disabled={twoFactorForm.processing || twoFactorForm.data.code.length < 6}
|
||||
className="w-full h-11 bg-[#3D4E4B] text-white text-xs font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60">
|
||||
{twoFactorForm.processing ? 'Verifying...' : 'Enable Two-Factor Authentication'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white rounded-2xl border border-emerald-100 shadow-sm p-6 flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
|
||||
<svg className="w-6 h-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-bold text-[#3D4E4B]">Two-Factor Authentication Aktif</div>
|
||||
<div className="text-xs text-gray-400 font-medium mt-0.5">Akun Anda dilindungi dengan TOTP authentication.</div>
|
||||
</div>
|
||||
<button onClick={handleDisable2FA} type="button"
|
||||
className="h-9 px-5 text-xs font-bold text-red-500 border border-red-200 rounded-xl hover:bg-red-50 transition-all">
|
||||
Disable 2FA
|
||||
</button>
|
||||
</div>
|
||||
<SectionCard title="Recovery Codes" description="Simpan kode ini dengan aman — gunakan jika kehilangan akses ke authenticator">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => setShowCodes(!showCodes)} type="button"
|
||||
className="h-8 px-4 text-xs font-bold text-[#3D4E4B] border border-gray-200 rounded-lg hover:bg-gray-50 transition-all">
|
||||
{showCodes ? 'Hide' : 'Show Codes'}
|
||||
</button>
|
||||
<button onClick={handleRegenerate} type="button"
|
||||
className="h-8 px-4 text-xs font-bold text-[#D4A017] border border-amber-200 rounded-lg hover:bg-amber-50 transition-all">
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
{showCodes && twoFactor.recovery_codes.length > 0 && (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{twoFactor.recovery_codes.map((code, i) => (
|
||||
<code key={i} className="px-4 py-2.5 bg-gray-50 border border-gray-100 rounded-xl text-xs font-mono text-[#3D4E4B] tracking-wider text-center">
|
||||
{code}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[11px] text-amber-600 font-semibold mt-3">⚠ Setiap kode hanya bisa digunakan sekali.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'danger' && (
|
||||
<div className="max-w-4xl anim-fade">
|
||||
<SectionCard title="Account Lifecycle" description="Critical and irreversible account actions" delay="0.1s">
|
||||
<div className="flex items-center justify-between gap-10 p-6 rounded-2xl bg-red-50 border border-red-100">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-14 h-14 rounded-2xl bg-white border border-red-100 flex items-center justify-center text-red-600 shrink-0">
|
||||
<svg className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base font-bold text-red-900 leading-none">Delete Account Permanently</p>
|
||||
<p className="text-xs text-red-700/60 font-semibold mt-2 leading-relaxed">
|
||||
Proceeding will scrub your identity, logs, and settings from our database. This action is irreversible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleDeleteAccount}
|
||||
className="h-11 px-8 bg-red-600 text-white text-xs font-bold uppercase tracking-widest rounded-xl hover:bg-red-700 transition-all shrink-0">
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user