- Integrated toast notifications to display error messages when form submissions fail across various components, enhancing user feedback and experience. - Updated components including DeleteUser, FormDialog, ManageTwoFactor, TwoFactorRecoveryCodes, TwoFactorSetupModal, PayrollPeriodShow, EmployeeCreate, EmployeeEdit, CuttingCreate, CuttingEdit, PurchaseCreate, PurchaseEdit, RestockCreate, RestockEdit, TransactionCreate, TransactionEdit, Category management, Product management, Raw Material management, Role management, Settings, and Authentication pages.
169 lines
7.1 KiB
TypeScript
169 lines
7.1 KiB
TypeScript
import { Form } from '@inertiajs/react';
|
|
import { Eye, EyeOff, LockKeyhole, RefreshCw } from 'lucide-react';
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import AlertError from '@/components/alert-error';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { regenerateRecoveryCodes } from '@/routes/two-factor';
|
|
|
|
type Props = {
|
|
recoveryCodesList: string[];
|
|
fetchRecoveryCodes: () => Promise<void>;
|
|
errors: string[];
|
|
};
|
|
|
|
export default function TwoFactorRecoveryCodes({
|
|
recoveryCodesList,
|
|
fetchRecoveryCodes,
|
|
errors,
|
|
}: Props) {
|
|
const [codesAreVisible, setCodesAreVisible] = useState<boolean>(false);
|
|
const codesSectionRef = useRef<HTMLDivElement | null>(null);
|
|
const canRegenerateCodes = recoveryCodesList.length > 0 && codesAreVisible;
|
|
|
|
const toggleCodesVisibility = useCallback(async () => {
|
|
if (!codesAreVisible && !recoveryCodesList.length) {
|
|
await fetchRecoveryCodes();
|
|
}
|
|
|
|
setCodesAreVisible(!codesAreVisible);
|
|
|
|
if (!codesAreVisible) {
|
|
setTimeout(() => {
|
|
codesSectionRef.current?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest',
|
|
});
|
|
});
|
|
}
|
|
}, [codesAreVisible, recoveryCodesList.length, fetchRecoveryCodes]);
|
|
|
|
useEffect(() => {
|
|
if (!recoveryCodesList.length) {
|
|
fetchRecoveryCodes();
|
|
}
|
|
}, [recoveryCodesList.length, fetchRecoveryCodes]);
|
|
|
|
const RecoveryCodeIconComponent = codesAreVisible ? EyeOff : Eye;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex gap-3">
|
|
<LockKeyhole className="size-4" aria-hidden="true" />
|
|
2FA recovery codes
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Recovery codes let you regain access if you lose your 2FA
|
|
device. Store them in a secure password manager.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between">
|
|
<Button
|
|
onClick={toggleCodesVisibility}
|
|
className="w-fit"
|
|
aria-expanded={codesAreVisible}
|
|
aria-controls="recovery-codes-section"
|
|
>
|
|
<RecoveryCodeIconComponent
|
|
className="size-4"
|
|
aria-hidden="true"
|
|
/>
|
|
{codesAreVisible ? 'Hide' : 'View'} recovery codes
|
|
</Button>
|
|
|
|
{canRegenerateCodes && (
|
|
<Form
|
|
{...regenerateRecoveryCodes.form()}
|
|
options={{ preserveScroll: true }}
|
|
onSuccess={fetchRecoveryCodes}
|
|
onError={() => {
|
|
toast.error('Terjadi kesalahan saat menyimpan data. Silakan periksa kembali input Anda.');
|
|
}}
|
|
>
|
|
{({ processing }) => (
|
|
<Button
|
|
variant="secondary"
|
|
type="submit"
|
|
disabled={processing}
|
|
aria-describedby="regenerate-warning"
|
|
>
|
|
<RefreshCw /> Regenerate codes
|
|
</Button>
|
|
)}
|
|
</Form>
|
|
)}
|
|
</div>
|
|
<div
|
|
id="recovery-codes-section"
|
|
className={`relative overflow-hidden transition-all duration-300 ${codesAreVisible ? 'h-auto opacity-100' : 'h-0 opacity-0'}`}
|
|
aria-hidden={!codesAreVisible}
|
|
>
|
|
<div className="mt-3 space-y-3">
|
|
{errors?.length ? (
|
|
<AlertError errors={errors} />
|
|
) : (
|
|
<>
|
|
<div
|
|
ref={codesSectionRef}
|
|
className="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm"
|
|
role="list"
|
|
aria-label="Recovery codes"
|
|
>
|
|
{recoveryCodesList.length ? (
|
|
recoveryCodesList.map((code, index) => (
|
|
<div
|
|
key={index}
|
|
role="listitem"
|
|
className="select-text"
|
|
>
|
|
{code}
|
|
</div>
|
|
))
|
|
) : (
|
|
<div
|
|
className="space-y-2"
|
|
aria-label="Loading recovery codes"
|
|
>
|
|
{Array.from(
|
|
{ length: 8 },
|
|
(_, index) => (
|
|
<div
|
|
key={index}
|
|
className="h-4 animate-pulse rounded bg-muted-foreground/20"
|
|
aria-hidden="true"
|
|
/>
|
|
),
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground select-none">
|
|
<p id="regenerate-warning">
|
|
Each recovery code can be used once to
|
|
access your account and will be removed
|
|
after use. If you need more, click{' '}
|
|
<span className="font-bold">
|
|
Regenerate codes
|
|
</span>{' '}
|
|
above.
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|