import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation, Link } 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, ArrowLeft, LogIn, KeyRound, Fingerprint } from "lucide-react"; // Progress Indicator Component untuk Login (3 steps) const LoginProgressIndicator = ({ currentStep = 3, totalSteps = 3 }) => { 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; lastLoginAt?: string; } interface VerifyPINActionData { errors?: { pin?: 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/requestotpforlogin"); } // Simulasi data user - dalam implementasi nyata, ambil dari database return json({ phone, lastLoginAt: "2025-07-05T10:30:00Z" // contoh last login }); }; 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; // Validation const errors: { pin?: 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"; } if (Object.keys(errors).length > 0) { return json({ errors }, { status: 400 }); } // Simulasi verifikasi PIN - dalam implementasi nyata, hash dan compare dengan database const validPIN = "123456"; // Demo PIN if (pin !== validPIN) { return json( { errors: { pin: "PIN yang Anda masukkan salah. Silakan coba lagi." } }, { status: 401 } ); } // PIN valid, buat session dan redirect ke dashboard try { console.log("PIN verified for phone:", phone); // Simulasi delay dan create session await new Promise((resolve) => setTimeout(resolve, 1500)); // Dalam implementasi nyata: // const session = await getSession(request.headers.get("Cookie")); // session.set("pengelolaId", userId); // session.set("pengelolaPhone", phone); // session.set("loginTime", new Date().toISOString()); // Redirect ke dashboard pengelola return redirect("/pengelola/dashboard"); } catch (error) { return json( { errors: { general: "Gagal login. Silakan coba lagi." } }, { status: 500 } ); } }; export default function VerifyExistingPIN() { const { phone, lastLoginAt } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const [pin, setPin] = useState(["", "", "", "", "", ""]); const [showPin, setShowPin] = useState(false); const [attemptCount, setAttemptCount] = useState(0); const pinRefs = useRef<(HTMLInputElement | null)[]>([]); const isSubmitting = navigation.state === "submitting"; // Handle PIN input change const handlePinChange = (index: number, value: string) => { if (!/^\d*$/.test(value)) return; // Only allow digits const newPin = [...pin]; newPin[index] = value; setPin(newPin); // Auto-focus next input if (value && index < 5) { pinRefs.current[index + 1]?.focus(); } }; // Handle key down (backspace) const handleKeyDown = (index: number, e: React.KeyboardEvent) => { if (e.key === "Backspace" && !pin[index] && index > 0) { pinRefs.current[index - 1]?.focus(); } }; // Handle paste const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const pastedText = e.clipboardData.getData("text"); const digits = pastedText.replace(/\D/g, "").slice(0, 6); if (digits.length === 6) { const newPin = digits.split(""); setPin(newPin); pinRefs.current[5]?.focus(); } }; // Format phone display const formatPhone = (phoneNumber: string) => { if (phoneNumber.length <= 2) return phoneNumber; if (phoneNumber.length <= 5) return `${phoneNumber.substring(0, 2)} ${phoneNumber.substring(2)}`; if (phoneNumber.length <= 9) return `${phoneNumber.substring(0, 2)} ${phoneNumber.substring( 2, 5 )} ${phoneNumber.substring(5)}`; return `${phoneNumber.substring(0, 2)} ${phoneNumber.substring( 2, 5 )} ${phoneNumber.substring(5, 9)} ${phoneNumber.substring(9)}`; }; // Format last login const formatLastLogin = (dateString?: string) => { if (!dateString) return "Belum pernah login"; const date = new Date(dateString); const now = new Date(); const diffInHours = Math.floor( (now.getTime() - date.getTime()) / (1000 * 60 * 60) ); if (diffInHours < 1) return "Kurang dari 1 jam yang lalu"; if (diffInHours < 24) return `${diffInHours} jam yang lalu`; const diffInDays = Math.floor(diffInHours / 24); if (diffInDays === 1) return "Kemarin"; if (diffInDays < 7) return `${diffInDays} hari yang lalu`; return date.toLocaleDateString("id-ID", { year: "numeric", month: "long", day: "numeric" }); }; const fullPin = pin.join(""); // Track failed attempts if (actionData?.errors?.pin && attemptCount < 3) { // In real implementation, this would be tracked server-side } return (
{/* Progress Indicator */} {/* Welcome Back Card */}

Selamat Datang Kembali!

Login terakhir: {formatLastLogin(lastLoginAt)}

{/* Main Card */}

Masukkan PIN Anda

Akun: {formatPhone(phone)}

{/* 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)} onPaste={handlePaste} className={`w-12 h-12 text-center text-lg font-bold transition-all duration-200 ${ actionData?.errors?.pin ? "border-red-500 dark:border-red-400" : "" } focus:scale-105`} autoFocus={index === 0} /> ))}
{actionData?.errors?.pin && (

{actionData.errors.pin}

{attemptCount >= 2 && (

Akun akan dikunci sementara setelah 3 kali percobaan gagal

)}
)}

Tempel PIN atau ketik manual

{/* Security Info */}

Keamanan Terjamin

PIN Anda dienkripsi dengan standar keamanan tinggi. Jangan bagikan PIN kepada siapapun.

{/* Submit Button */}
{/* Forgot PIN */}
Lupa PIN?

Hubungi Customer Support untuk reset PIN

{/* Back Link */}
Kembali ke verifikasi OTP
{/* Demo Info */}

Demo PIN:

Gunakan PIN:{" "} 123456

Untuk testing login flow pengelola

{/* Security Notice */}

Tips Keamanan

  • • Jangan bagikan PIN kepada siapapun
  • • Logout dari perangkat yang bukan milik Anda
  • • Ganti PIN secara berkala untuk keamanan
  • • Laporkan aktivitas mencurigakan ke admin
); }