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 */}
{/* Final Note */}
🎉 Hampir selesai!
Setelah membuat PIN, Anda akan langsung dapat mengakses
dashboard pengelola
);
}