import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation } from "@remix-run/react"; import { useState, useRef } from "react"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Shield, CheckCircle, AlertCircle, Loader2, Lock, Eye, EyeOff, Sparkles } from "lucide-react"; // Progress Indicator Component const ProgressIndicator = ({ currentStep = 5, totalSteps = 5 }) => { return (
{Array.from({ length: totalSteps }, (_, index) => { const stepNumber = index + 1; const isActive = stepNumber === currentStep; const isCompleted = stepNumber < currentStep; return (
{isCompleted ? : stepNumber}
{stepNumber < totalSteps && (
)}
); })}
); }; // Interfaces interface LoaderData { phone: string; approvedAt: string; } interface CreatePINActionData { errors?: { pin?: string; confirmPin?: string; general?: string; }; success?: boolean; } export const loader = async ({ request }: LoaderFunctionArgs): Promise => { const url = new URL(request.url); const phone = url.searchParams.get("phone"); if (!phone) { return redirect("/authpengelola/requestotpforregister"); } return json({ phone, approvedAt: new Date().toISOString() }); }; export const action = async ({ request }: ActionFunctionArgs): Promise => { const formData = await request.formData(); const phone = formData.get("phone") as string; const pin = formData.get("pin") as string; const confirmPin = formData.get("confirmPin") as string; // Validation const errors: { pin?: string; confirmPin?: string; general?: string } = {}; if (!pin || pin.length !== 6) { errors.pin = "PIN harus 6 digit"; } else if (!/^\d{6}$/.test(pin)) { errors.pin = "PIN hanya boleh berisi angka"; } else if (/^(.)\1{5}$/.test(pin)) { errors.pin = "PIN tidak boleh angka yang sama semua (111111)"; } else if (pin === "123456" || pin === "654321" || pin === "000000") { errors.pin = "PIN terlalu mudah ditebak, gunakan kombinasi yang lebih aman"; } if (!confirmPin) { errors.confirmPin = "Konfirmasi PIN wajib diisi"; } else if (pin !== confirmPin) { errors.confirmPin = "PIN dan konfirmasi PIN tidak sama"; } if (Object.keys(errors).length > 0) { return json({ errors }, { status: 400 }); } // Simulasi menyimpan PIN - dalam implementasi nyata, hash dan simpan ke database try { console.log("Creating PIN for phone:", phone); // Simulasi delay API call await new Promise((resolve) => setTimeout(resolve, 1500)); // Redirect ke dashboard pengelola setelah berhasil return redirect("/pengelola/dashboard"); } catch (error) { return json( { errors: { general: "Gagal membuat PIN. Silakan coba lagi." } }, { status: 500 } ); } }; export default function CreateANewPIN() { const { phone, approvedAt } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const [pin, setPin] = useState(["", "", "", "", "", ""]); const [confirmPin, setConfirmPin] = useState(["", "", "", "", "", ""]); const [showPin, setShowPin] = useState(false); const [pinStrength, setPinStrength] = useState(0); const pinRefs = useRef<(HTMLInputElement | null)[]>([]); const confirmPinRefs = useRef<(HTMLInputElement | null)[]>([]); const isSubmitting = navigation.state === "submitting"; // Handle PIN input change const handlePinChange = ( index: number, value: string, isConfirm: boolean = false ) => { if (!/^\d*$/.test(value)) return; // Only allow digits const newPin = isConfirm ? [...confirmPin] : [...pin]; newPin[index] = value; if (isConfirm) { setConfirmPin(newPin); } else { setPin(newPin); calculatePinStrength(newPin.join("")); } // Auto-focus next input if (value && index < 5) { const refs = isConfirm ? confirmPinRefs : pinRefs; refs.current[index + 1]?.focus(); } }; // Handle key down (backspace) const handleKeyDown = ( index: number, e: React.KeyboardEvent, isConfirm: boolean = false ) => { if (e.key === "Backspace") { const currentPin = isConfirm ? confirmPin : pin; const refs = isConfirm ? confirmPinRefs : pinRefs; if (!currentPin[index] && index > 0) { refs.current[index - 1]?.focus(); } } }; // Calculate PIN strength const calculatePinStrength = (pinValue: string) => { if (pinValue.length < 6) { setPinStrength(0); return; } let strength = 0; // Check for sequential numbers const isSequential = /012345|123456|234567|345678|456789|987654|876543|765432|654321|543210/.test( pinValue ); if (!isSequential) strength += 25; // Check for repeated numbers const hasRepeated = /(.)\1{2,}/.test(pinValue); if (!hasRepeated) strength += 25; // Check for common patterns const isCommon = [ "123456", "654321", "111111", "000000", "222222", "333333", "444444", "555555", "666666", "777777", "888888", "999999" ].includes(pinValue); if (!isCommon) strength += 25; // Check for variety const uniqueDigits = new Set(pinValue.split("")).size; if (uniqueDigits >= 4) strength += 25; setPinStrength(strength); }; // Get strength color and text const getStrengthInfo = () => { if (pinStrength === 0) return { color: "bg-gray-200", text: "Masukkan PIN", textColor: "text-gray-500" }; if (pinStrength <= 25) return { color: "bg-red-500", text: "Lemah", textColor: "text-red-600" }; if (pinStrength <= 50) return { color: "bg-yellow-500", text: "Sedang", textColor: "text-yellow-600" }; if (pinStrength <= 75) return { color: "bg-blue-500", text: "Bagus", textColor: "text-blue-600" }; return { color: "bg-green-500", text: "Sangat Kuat", textColor: "text-green-600" }; }; const strengthInfo = getStrengthInfo(); const fullPin = pin.join(""); const fullConfirmPin = confirmPin.join(""); return (
{/* Progress Indicator */} {/* Success Alert */}

Selamat! Akun Anda Telah Disetujui

Administrator telah memverifikasi dan menyetujui aplikasi Anda

{/* Main Card */}

Buat PIN Keamanan

Langkah terakhir untuk mengamankan akun Anda

{/* Error Alert */} {actionData?.errors?.general && ( {actionData.errors.general} )} {/* Form */}
{/* PIN Input */}
{pin.map((digit, index) => ( (pinRefs.current[index] = el)} type={showPin ? "text" : "password"} maxLength={1} value={digit} onChange={(e) => handlePinChange(index, e.target.value)} onKeyDown={(e) => handleKeyDown(index, e)} className={`w-12 h-12 text-center text-lg font-bold ${ actionData?.errors?.pin ? "border-red-500" : "" }`} autoFocus={index === 0} /> ))}
{actionData?.errors?.pin && (

{actionData.errors.pin}

)} {/* PIN Strength Indicator */} {fullPin.length > 0 && (
Kekuatan PIN {strengthInfo.text}
)}
{/* Confirm PIN Input */}
{confirmPin.map((digit, index) => ( (confirmPinRefs.current[index] = el)} type={showPin ? "text" : "password"} maxLength={1} value={digit} onChange={(e) => handlePinChange(index, e.target.value, true) } onKeyDown={(e) => handleKeyDown(index, e, true)} className={`w-12 h-12 text-center text-lg font-bold ${ actionData?.errors?.confirmPin ? "border-red-500" : "" }`} /> ))}
{actionData?.errors?.confirmPin && (

{actionData.errors.confirmPin}

)} {/* PIN Match Indicator */} {fullPin.length === 6 && fullConfirmPin.length === 6 && (
{fullPin === fullConfirmPin ? (
PIN cocok
) : (
PIN tidak cocok
)}
)}
{/* PIN Guidelines */}

Tips PIN yang Aman:

  • • Hindari angka berurutan (123456, 654321)
  • • Jangan gunakan angka yang sama semua (111111)
  • • Hindari kombinasi mudah ditebak (000000, 123456)
  • • Gunakan kombinasi angka yang hanya Anda ketahui
{/* Submit Button */}
{/* Final Note */}

🎉 Hampir selesai!

Setelah membuat PIN, Anda akan langsung dapat mengakses dashboard pengelola

); }