a tool for shared writing and social publishing
1"use client";
2import {
3 confirmEmailAuthToken,
4 requestAuthEmailToken,
5} from "actions/emailAuth";
6import { loginWithEmailToken } from "actions/login";
7import { ActionAfterSignIn } from "app/api/oauth/[route]/afterSignInActions";
8import { getHomeDocs } from "app/home/storage";
9import { ButtonPrimary } from "components/Buttons";
10import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
11import { BlueskySmall } from "components/Icons/BlueskySmall";
12import { Input } from "components/Input";
13import { useSmoker, useToaster } from "components/Toast";
14import React, { useState } from "react";
15import { mutate } from "swr";
16
17export default function LoginForm(props: {
18 noEmail?: boolean;
19 redirectRoute?: string;
20 action?: ActionAfterSignIn;
21 text: React.ReactNode;
22}) {
23 type FormState =
24 | {
25 stage: "email";
26 email: string;
27 }
28 | {
29 stage: "code";
30 email: string;
31 tokenId: string;
32 confirmationCode: string;
33 };
34
35 const [formState, setFormState] = useState<FormState>({
36 stage: "email",
37 email: "",
38 });
39
40 const handleSubmitEmail = async (e: React.FormEvent) => {
41 e.preventDefault();
42 const tokenId = await requestAuthEmailToken(formState.email);
43 setFormState({
44 stage: "code",
45 email: formState.email,
46 tokenId,
47 confirmationCode: "",
48 });
49 };
50
51 let smoker = useSmoker();
52 let toaster = useToaster();
53
54 const handleSubmitCode = async (e: React.FormEvent) => {
55 e.preventDefault();
56 let rect = e.currentTarget.getBoundingClientRect();
57
58 if (formState.stage !== "code") return;
59 const confirmedToken = await confirmEmailAuthToken(
60 formState.tokenId,
61 formState.confirmationCode,
62 );
63
64 if (!confirmedToken) {
65 smoker({
66 error: true,
67 text: "incorrect code!",
68 position: {
69 y: rect.bottom - 16,
70 x: rect.right - 220,
71 },
72 });
73 } else {
74 let localLeaflets = getHomeDocs();
75
76 await loginWithEmailToken(localLeaflets.filter((l) => !l.hidden));
77 mutate("identity");
78 toaster({
79 content: <div className="font-bold">Logged in! Welcome!</div>,
80 type: "success",
81 });
82 }
83 };
84
85 if (formState.stage === "code") {
86 return (
87 <div className="w-full max-w-md flex flex-col gap-3 py-1">
88 <div className=" text-secondary font-bold">
89 Please enter the code we sent to
90 <div className="italic truncate">{formState.email}</div>
91 </div>
92 <form onSubmit={handleSubmitCode} className="flex flex-col gap-2 ">
93 <Input
94 type="text"
95 className="input-with-border"
96 placeholder="000000"
97 value={formState.confirmationCode}
98 onChange={(e) =>
99 setFormState({
100 ...formState,
101 confirmationCode: e.target.value,
102 })
103 }
104 required
105 />
106
107 <ButtonPrimary
108 type="submit"
109 className="place-self-end"
110 disabled={formState.confirmationCode === ""}
111 onMouseDown={(e) => {}}
112 >
113 Confirm
114 </ButtonPrimary>
115 </form>
116 </div>
117 );
118 }
119
120 return (
121 <div className="flex flex-col gap-3 w-full max-w-xs pb-1">
122 <div className="flex flex-col">
123 <h4 className="text-primary">Log In or Sign Up</h4>
124 <div className=" text-tertiary text-sm">{props.text}</div>
125 </div>
126
127 <BlueskyLogin {...props} />
128
129 {props.noEmail ? null : (
130 <>
131 <div className="flex gap-2 text-border italic w-full items-center">
132 <hr className="border-border-light w-full" />
133 <div>or</div>
134 <hr className="border-border-light w-full" />
135 </div>
136 <form
137 onSubmit={handleSubmitEmail}
138 className="flex flex-col gap-2 relative"
139 >
140 <Input
141 type="email"
142 placeholder="email@example.com"
143 value={formState.email}
144 className="input-with-border p-7"
145 onChange={(e) =>
146 setFormState({
147 ...formState,
148 email: e.target.value,
149 })
150 }
151 required
152 />
153
154 <ButtonPrimary
155 type="submit"
156 className="place-self-end px-[2px]! absolute right-1 bottom-1"
157 >
158 <ArrowRightTiny />{" "}
159 </ButtonPrimary>
160 </form>
161 </>
162 )}
163 </div>
164 );
165}
166
167export function BlueskyLogin(props: {
168 redirectRoute?: string;
169 action?: ActionAfterSignIn;
170}) {
171 const [signingWithHandle, setSigningWithHandle] = useState(false);
172 const [handle, setHandle] = useState("");
173
174 return (
175 <form action={`/api/oauth/login`} method="GET">
176 <input
177 type="hidden"
178 name="redirect_url"
179 value={props.redirectRoute || "/"}
180 />
181 {props.action && (
182 <input
183 type="hidden"
184 name="action"
185 value={JSON.stringify(props.action)}
186 />
187 )}
188 {signingWithHandle ? (
189 <div className="w-full flex flex-col gap-2">
190 <Input
191 type="text"
192 name="handle"
193 id="handle"
194 placeholder="you.bsky.social"
195 value={handle}
196 className="input-with-border"
197 onChange={(e) => setHandle(e.target.value)}
198 required
199 />
200 <ButtonPrimary type="submit" fullWidth className="py-2">
201 <BlueskySmall />
202 Sign In
203 </ButtonPrimary>
204 </div>
205 ) : (
206 <div className="flex flex-col">
207 <ButtonPrimary fullWidth className="py-2">
208 <BlueskySmall />
209 Log In/Sign Up with Bluesky
210 </ButtonPrimary>
211 <button
212 type="button"
213 className="text-sm text-accent-contrast place-self-center mt-[6px]"
214 onClick={() => setSigningWithHandle(true)}
215 >
216 or use an ATProto handle
217 </button>
218 </div>
219 )}
220 </form>
221 );
222}