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}