Graphical PDS migrator for AT Protocol
1import { useState } from "preact/hooks"; 2import { JSX } from "preact"; 3 4/** 5 * The credential login form. 6 * @returns The credential login form 7 * @component 8 */ 9export default function CredLogin() { 10 const [handle, setHandle] = useState(""); 11 const [password, setPassword] = useState(""); 12 const [error, setError] = useState<string | null>(null); 13 const [isPending, setIsPending] = useState(false); 14 15 const handleSubmit = async (e: JSX.TargetedEvent<HTMLFormElement>) => { 16 e.preventDefault(); 17 if (!handle.trim() || !password.trim()) return; 18 19 setError(null); 20 setIsPending(true); 21 22 try { 23 const response = await fetch("/api/cred/login", { 24 method: "POST", 25 headers: { 26 "Content-Type": "application/json", 27 }, 28 body: JSON.stringify({ handle, password }), 29 }); 30 31 if (!response.ok) { 32 const errorText = await response.text(); 33 throw new Error(errorText || "Login failed"); 34 } 35 36 // Add a small delay before redirecting for better UX 37 await new Promise((resolve) => setTimeout(resolve, 500)); 38 39 // Redirect to home page after successful login 40 globalThis.location.href = "/"; 41 } catch (err) { 42 const message = err instanceof Error ? err.message : "Login failed"; 43 setError(message); 44 } finally { 45 setIsPending(false); 46 } 47 }; 48 49 return ( 50 <form onSubmit={handleSubmit}> 51 {error && ( 52 <div className="text-red-500 mb-4 p-2 bg-red-50 dark:bg-red-950 dark:bg-opacity-30 rounded-md"> 53 {error} 54 </div> 55 )} 56 57 <div className="mb-4"> 58 <label 59 htmlFor="handle" 60 className="block mb-2 text-gray-700 dark:text-gray-300" 61 > 62 Enter your Bluesky handle: 63 </label> 64 <input 65 id="handle" 66 type="text" 67 value={handle} 68 onInput={(e) => setHandle((e.target as HTMLInputElement).value)} 69 placeholder="example.bsky.social" 70 disabled={isPending} 71 className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors" 72 /> 73 </div> 74 75 <div className="mb-4"> 76 <label 77 htmlFor="password" 78 className="block mb-2 text-gray-700 dark:text-gray-300" 79 > 80 Password: 81 </label> 82 <input 83 id="password" 84 type="password" 85 value={password} 86 onInput={(e) => setPassword((e.target as HTMLInputElement).value)} 87 placeholder="Enter your account password" 88 disabled={isPending} 89 className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors" 90 /> 91 <p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> 92 This is your main account password, not an app password. This is 93 required for migrations. 94 <br /> 95 Ensure that 2 Factor Authentication is turned off before proceeding. 96 You can turn it back on after the migration is complete. 97 </p> 98 </div> 99 100 <button 101 type="submit" 102 disabled={isPending} 103 className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 relative ${ 104 isPending ? "opacity-90 cursor-not-allowed" : "" 105 }`} 106 > 107 <span className={isPending ? "invisible" : ""}> 108 Login with Password 109 </span> 110 {isPending && ( 111 <span className="absolute inset-0 flex items-center justify-center"> 112 <svg 113 className="animate-spin -ml-1 mr-2 h-5 w-5 text-white" 114 xmlns="http://www.w3.org/2000/svg" 115 fill="none" 116 viewBox="0 0 24 24" 117 > 118 <circle 119 className="opacity-25" 120 cx="12" 121 cy="12" 122 r="10" 123 stroke="currentColor" 124 strokeWidth="4" 125 > 126 </circle> 127 <path 128 className="opacity-75" 129 fill="currentColor" 130 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 131 > 132 </path> 133 </svg> 134 <span>Logging in...</span> 135 </span> 136 )} 137 </button> 138 </form> 139 ); 140}