the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use react-hook-form and Zod for sign-in

+135 -95
+99 -72
apps/web/src/components/signin/SignIn.tsx
··· 1 - import { useEffect, useRef, useState } from "react"; 1 + import { useEffect, useRef } from "react"; 2 + import { useForm } from "react-hook-form"; 3 + import { zodResolver } from "@hookform/resolvers/zod"; 4 + import { z } from "zod"; 2 5 import { API_URL } from "../../consts"; 3 6 4 7 export type SignInProps = { 5 8 isOpen: boolean; 6 9 onClose: () => void; 7 10 }; 11 + 12 + const signInSchema = z.object({ 13 + handle: z.string().trim().min(1, { message: "Handle is required" }), 14 + }); 15 + 16 + type SignInFormValues = z.infer<typeof signInSchema>; 8 17 9 18 function SignIn({ isOpen, onClose }: SignInProps) { 10 - const [handle, setHandle] = useState(""); 11 19 const inputRef = useRef<HTMLInputElement>(null); 12 20 13 - const onSignIn = () => { 14 - if (!handle) { 15 - return; 16 - } 17 - window.location.href = `${API_URL}/login?handle=${handle}`; 21 + const { 22 + register, 23 + handleSubmit, 24 + reset, 25 + formState: { errors }, 26 + } = useForm<SignInFormValues>({ 27 + resolver: zodResolver(signInSchema), 28 + defaultValues: { 29 + handle: "", 30 + }, 31 + }); 32 + 33 + const { ref: registerRef, ...registerRest } = register("handle"); 34 + 35 + const onSubmit = (data: SignInFormValues) => { 36 + window.location.href = `${API_URL}/login?handle=${data.handle}`; 37 + onClose(); 38 + }; 39 + 40 + const handleClose = () => { 41 + reset(); 18 42 onClose(); 19 43 }; 20 44 21 45 const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { 22 46 if (e.target === e.currentTarget) { 23 - onClose(); 47 + handleClose(); 24 48 } 25 49 }; 26 50 ··· 33 57 useEffect(() => { 34 58 const handleEscapeKey = (event: KeyboardEvent) => { 35 59 if (event.key === "Escape" && isOpen) { 36 - setHandle(""); 37 - onClose(); 38 - } 39 - if (event.key === "Enter" && isOpen && handle) { 40 - onSignIn(); 60 + handleClose(); 41 61 } 42 62 }; 43 63 ··· 45 65 return () => { 46 66 document.removeEventListener("keydown", handleEscapeKey); 47 67 }; 48 - }, [isOpen, onClose, handle, onSignIn]); 68 + }, [isOpen, onClose, handleClose]); 49 69 50 70 return ( 51 71 <> ··· 62 82 type="button" 63 83 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" 64 84 aria-label="Close" 65 - onClick={() => { 66 - onClose(); 67 - setHandle(""); 68 - }} 85 + onClick={handleClose} 69 86 > 70 87 <span className="icon-[tabler--x] size-4"></span> 71 88 </button> 72 89 </div> 73 90 <div className="modal-body"> 74 - <div className="flex flex-col items-center gap-6 w-[400px]"> 75 - <div className="form-control w-full"> 76 - <label className="label"> 77 - <span className="label-text text-[15px]">Handle</span> 78 - </label> 79 - <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent"> 80 - <span className="label-text my-auto text-[16px] opacity-50 mr-[10px]"> 81 - @ 82 - </span> 83 - <input 84 - placeholder="alice.bsky.social" 85 - className="grow " 86 - ref={inputRef} 87 - value={handle} 88 - onChange={(e) => setHandle(e.target.value)} 89 - autoFocus 90 - /> 91 + <form onSubmit={handleSubmit(onSubmit)} noValidate> 92 + <div className="flex flex-col items-center gap-6 w-[400px]"> 93 + <div className="form-control w-full"> 94 + <label className="label"> 95 + <span className="label-text text-[15px]">Handle</span> 96 + </label> 97 + <div 98 + className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent ${errors.handle ? "input-error" : ""}`} 99 + > 100 + <span className="label-text my-auto text-[16px] opacity-50 mr-[10px]"> 101 + @ 102 + </span> 103 + <input 104 + {...registerRest} 105 + ref={(el) => { 106 + registerRef(el); 107 + ( 108 + inputRef as React.MutableRefObject<HTMLInputElement | null> 109 + ).current = el; 110 + }} 111 + placeholder="alice.bsky.social" 112 + className="grow" 113 + autoFocus 114 + /> 115 + </div> 116 + {errors.handle && ( 117 + <span className="label-text text-error text-sm mt-1"> 118 + {errors.handle.message} 119 + </span> 120 + )} 91 121 </div> 92 - </div> 93 122 94 - <button 95 - className="btn btn-lg font-bold btn-primary border-none w-full" 96 - onClick={onSignIn} 97 - > 98 - Sign In 99 - </button> 100 - <p className="text-center font-semibold text-white/70"> 101 - Don't have an atproto handle yet? You can create one at 102 - <a 103 - href={`${API_URL}/login?prompt=create`} 104 - className="text-primary" 123 + <button 124 + type="submit" 125 + className="btn btn-lg font-bold btn-primary border-none w-full" 105 126 > 106 - selfhosted.social 107 - </a> 108 - ,{" "} 109 - <a 110 - href="https://bsky.app/" 111 - className="text-primary" 112 - target="_blank" 113 - > 114 - Bluesky 115 - </a>{" "} 116 - or any other{" "} 117 - <a 118 - href={"https://atproto.com"} 119 - className="text-primary" 120 - target="_blank" 121 - > 122 - AT Protocol 123 - </a>{" "} 124 - service. 125 - </p> 126 - </div> 127 + Sign In 128 + </button> 129 + <p className="text-center font-semibold text-white/70"> 130 + Don't have an atproto handle yet? You can create one at{" "} 131 + <a 132 + href={`${API_URL}/login?prompt=create`} 133 + className="text-primary" 134 + > 135 + selfhosted.social 136 + </a> 137 + ,{" "} 138 + <a 139 + href="https://bsky.app/" 140 + className="text-primary" 141 + target="_blank" 142 + > 143 + Bluesky 144 + </a>{" "} 145 + or any other{" "} 146 + <a 147 + href={"https://atproto.com"} 148 + className="text-primary" 149 + target="_blank" 150 + > 151 + AT Protocol 152 + </a>{" "} 153 + service. 154 + </p> 155 + </div> 156 + </form> 127 157 </div> 128 158 </div> 129 159 </div> ··· 135 165 data-overlay-backdrop-template="" 136 166 style={{ zIndex: 79 }} 137 167 className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75" 138 - onClick={() => { 139 - setHandle(""); 140 - onClose(); 141 - }} 168 + onClick={handleClose} 142 169 ></div> 143 170 )} 144 171 </>
+36 -23
apps/web/src/pages/signin/SignIn.tsx
··· 1 - import { useState } from "react"; 1 + import { useForm } from "react-hook-form"; 2 + import { zodResolver } from "@hookform/resolvers/zod"; 3 + import { z } from "zod"; 2 4 import { API_URL } from "../../consts"; 3 5 6 + const signInSchema = z.object({ 7 + handle: z.string().trim().min(1, { message: "Handle is required" }), 8 + }); 9 + 10 + type SignInFormValues = z.infer<typeof signInSchema>; 11 + 4 12 function SignIn() { 5 - const [handle, setHandle] = useState(""); 13 + const { 14 + register, 15 + handleSubmit, 16 + formState: { errors }, 17 + } = useForm<SignInFormValues>({ 18 + resolver: zodResolver(signInSchema), 19 + defaultValues: { 20 + handle: "", 21 + }, 22 + }); 6 23 7 - const onSignIn = () => { 8 - if (!handle) { 9 - return; 10 - } 11 - window.location.href = `${API_URL}/login?handle=${handle}`; 12 - }; 13 - 14 - const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { 15 - if (event.key === "Enter" && handle) { 16 - onSignIn(); 17 - } 24 + const onSubmit = (data: SignInFormValues) => { 25 + window.location.href = `${API_URL}/login?handle=${data.handle}`; 18 26 }; 19 27 20 28 return ( 21 - <> 22 - <div className="flex items-center justify-center min-h-screen bg-base-100"> 29 + <div className="flex items-center justify-center min-h-screen bg-base-100"> 30 + <form onSubmit={handleSubmit(onSubmit)} noValidate> 23 31 <div className="flex flex-col items-center gap-6 w-[400px]"> 24 32 <div className="form-control w-full"> 25 33 <label className="label"> 26 34 <span className="label-text text-[15px]">Handle</span> 27 35 </label> 28 - <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent "> 36 + <div 37 + className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent ${errors.handle ? "input-error" : ""}`} 38 + > 29 39 <span className="label-text my-auto text-[16px] opacity-50 mr-[10px]"> 30 40 @ 31 41 </span> 32 42 <input 43 + {...register("handle")} 33 44 placeholder="alice.bsky.social" 34 - className="grow " 35 - value={handle} 36 - onChange={(e) => setHandle(e.target.value.trim())} 37 - onKeyDown={handleKeyDown} 45 + className="grow" 38 46 autoFocus 39 47 /> 40 48 </div> 49 + {errors.handle && ( 50 + <span className="label-text text-error text-sm mt-1"> 51 + {errors.handle.message} 52 + </span> 53 + )} 41 54 </div> 42 55 43 56 <button 57 + type="submit" 44 58 className="btn btn-lg font-bold btn-primary border-none w-full" 45 - onClick={onSignIn} 46 59 > 47 60 Sign In 48 61 </button> ··· 70 83 service. 71 84 </p> 72 85 </div> 73 - </div> 74 - </> 86 + </form> 87 + </div> 75 88 ); 76 89 } 77 90