an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
1import React, { useEffect, useState, useRef } from "react";
2import { useAuth } from "~/providers/PassAuthProvider";
3
4interface LoginProps {
5 compact?: boolean;
6}
7
8export default function Login({ compact = false }: LoginProps) {
9 const { loginStatus, login, logout, loading, authed } = useAuth();
10 const [user, setUser] = useState("");
11 const [password, setPassword] = useState("");
12 const [serviceURL, setServiceURL] = useState("bsky.social");
13 const [showLoginForm, setShowLoginForm] = useState(false);
14 const formRef = useRef<HTMLDivElement>(null);
15
16 useEffect(() => {
17 function handleClickOutside(event: MouseEvent) {
18 if (formRef.current && !formRef.current.contains(event.target as Node)) {
19 setShowLoginForm(false);
20 }
21 }
22
23 if (showLoginForm) {
24 document.addEventListener("mousedown", handleClickOutside);
25 }
26
27 return () => {
28 document.removeEventListener("mousedown", handleClickOutside);
29 };
30 }, [showLoginForm]);
31
32 if (loading) {
33 return (
34 <div className="flex items-center justify-center p-6 text-gray-500 dark:text-gray-400">
35 Loading...
36 </div>
37 );
38 }
39
40 if (compact) {
41 if (authed) {
42 return (
43 <button
44 onClick={logout}
45 className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
46 >
47 Log out
48 </button>
49 );
50 } else {
51 return (
52 <div className="relative" ref={formRef}>
53 <button
54 onClick={() => setShowLoginForm(!showLoginForm)}
55 className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
56 >
57 Log in
58 </button>
59 {showLoginForm && (
60 <div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
61 <form
62 onSubmit={(e) => {
63 e.preventDefault();
64 login(user, password, `https://${serviceURL}`);
65 setShowLoginForm(false);
66 }}
67 className="flex flex-col gap-3"
68 >
69 <p className="text-xs text-gray-500 dark:text-gray-400">
70 sorry for the temporary login,
71 <br />
72 oauth will come soon enough i swear
73 </p>
74 <input
75 type="text"
76 placeholder="Username"
77 value={user}
78 onChange={(e) => setUser(e.target.value)}
79 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
80 autoComplete="username"
81 />
82 <input
83 type="password"
84 placeholder="Password"
85 value={password}
86 onChange={(e) => setPassword(e.target.value)}
87 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
88 autoComplete="current-password"
89 />
90 <input
91 type="text"
92 placeholder="bsky.social"
93 value={serviceURL}
94 onChange={(e) => setServiceURL(e.target.value)}
95 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
96 />
97 <button
98 type="submit"
99 className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors"
100 >
101 Log in
102 </button>
103 </form>
104 </div>
105 )}
106 </div>
107 );
108 }
109 }
110
111 return (
112 <div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
113 {authed ? (
114 <div className="flex flex-col items-center justify-center text-center">
115 <p className="text-lg font-semibold mb-6 text-gray-800 dark:text-gray-100">
116 You are logged in!
117 </p>
118 <button
119 onClick={logout}
120 className="bg-gray-600 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors"
121 >
122 Log out
123 </button>
124 </div>
125 ) : (
126 <form
127 onSubmit={(e) => {
128 e.preventDefault();
129 login(user, password, `https://${serviceURL}`);
130 }}
131 className="flex flex-col gap-4"
132 >
133 <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
134 sorry for the temporary login,
135 <br />
136 oauth will come soon enough i swear
137 </p>
138 <input
139 type="text"
140 placeholder="Username"
141 value={user}
142 onChange={(e) => setUser(e.target.value)}
143 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
144 autoComplete="username"
145 />
146 <input
147 type="password"
148 placeholder="Password"
149 value={password}
150 onChange={(e) => setPassword(e.target.value)}
151 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
152 autoComplete="current-password"
153 />
154 <input
155 type="text"
156 placeholder="bsky.social"
157 value={serviceURL}
158 onChange={(e) => setServiceURL(e.target.value)}
159 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
160 />
161 <button
162 type="submit"
163 className="bg-gray-600 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors mt-2"
164 >
165 Log in
166 </button>
167 </form>
168 )}
169 </div>
170 );
171}
172
173export const ProfileThing = () => {
174 const { agent, loading, loginStatus, authed } = useAuth();
175 const [response, setResponse] = useState<any>(null);
176
177 useEffect(() => {
178 if (loginStatus && agent && !loading && authed) {
179 fetchUser();
180 }
181 // eslint-disable-next-line
182 }, [loginStatus, agent, loading, authed]);
183
184 const fetchUser = async () => {
185 if (!agent) {
186 console.error("Agent is null or undefined");
187 return;
188 }
189 const res = await agent.app.bsky.actor.getProfile({
190 actor: agent.assertDid,
191 });
192 setResponse(res.data);
193 };
194
195 if (!authed) {
196 return (
197 <div className="inline-block">
198 <span className="text-gray-100 text-base font-medium px-1.5">
199 Login
200 </span>
201 </div>
202 );
203 }
204
205 if (!response) {
206 return (
207 <div className="flex flex-col items-start gap-1.5">
208 <span className="w-5 h-5 border-2 border-gray-200 dark:border-gray-600 border-t-transparent rounded-full animate-spin inline-block" />
209 <span className="text-gray-100">Loading... </span>
210 </div>
211 );
212 }
213
214 return (
215 <div className="flex flex-row items-start gap-1.5">
216 <img
217 src={response?.avatar}
218 alt="avatar"
219 className="w-[30px] h-[30px] rounded-full object-cover"
220 />
221 <div>
222 <div className="text-gray-100 text-xs">{response?.displayName}</div>
223 <div className="text-gray-100 text-xs">@{response?.handle}</div>
224 </div>
225 </div>
226 );
227};