a tool for shared writing and social publishing
at refactor/shared-home-layout 222 lines 6.2 kB view raw
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-pages)/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}