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 */}
{/* Forgot 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
);
}