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