ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

add @ symbol and profile pic to login input

Created HandleInput component:
- Shows @ symbol by default
- Replaces @ with profile pic when handle selected from typeahead
- Extracts avatar from typeahead data-avatar or actor-select event
- Clears avatar when input is cleared

byarielm.fyi 6f618f32 bab2fb88

verified
Changed files
+84 -8
src
components
pages
+56
src/components/login/HandleInput.tsx
··· 1 + import { forwardRef, useState, useEffect } from "react"; 2 + import { AtSign } from "lucide-react"; 3 + 4 + interface HandleInputProps 5 + extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> { 6 + error?: boolean; 7 + selectedAvatar?: string | null; 8 + } 9 + 10 + const HandleInput = forwardRef<HTMLInputElement, HandleInputProps>( 11 + ({ error, selectedAvatar, className, ...props }, ref) => { 12 + const [showAvatar, setShowAvatar] = useState(false); 13 + 14 + useEffect(() => { 15 + // Show avatar when one is selected 16 + if (selectedAvatar) { 17 + setShowAvatar(true); 18 + } else { 19 + setShowAvatar(false); 20 + } 21 + }, [selectedAvatar]); 22 + 23 + return ( 24 + <div className="relative"> 25 + {/* @ symbol or Profile pic */} 26 + <div className="absolute left-3 top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 pointer-events-none z-10"> 27 + {showAvatar && selectedAvatar ? ( 28 + <img 29 + src={selectedAvatar} 30 + alt="Selected profile" 31 + className="w-8 h-8 rounded-full object-cover border-2 border-cyan-500/50 dark:border-purple-500/50" 32 + /> 33 + ) : ( 34 + <AtSign className="w-5 h-5 text-purple-750/60 dark:text-cyan-250/60" /> 35 + )} 36 + </div> 37 + 38 + {/* Input field */} 39 + <input 40 + ref={ref} 41 + type="text" 42 + className={`w-full pl-14 pr-4 py-3 bg-purple-50/50 dark:bg-slate-900/50 border-2 rounded-xl text-purple-900 dark:text-cyan-100 placeholder-purple-750/80 dark:placeholder-cyan-250/80 focus:outline-none focus:ring-2 transition-all ${ 43 + error 44 + ? "border-red-500 focus:ring-red-500" 45 + : "border-cyan-500/50 dark:border-purple-500/30 focus:ring-orange-500 dark:focus:ring-amber-400 focus:border-transparent" 46 + } ${className || ""}`} 47 + {...props} 48 + /> 49 + </div> 50 + ); 51 + } 52 + ); 53 + 54 + HandleInput.displayName = "HandleInput"; 55 + 56 + export default HandleInput;
+28 -8
src/pages/Login.tsx
··· 6 6 import HeroSection from "../components/login/HeroSection"; 7 7 import ValuePropsSection from "../components/login/ValuePropsSection"; 8 8 import HowItWorksSection from "../components/login/HowItWorksSection"; 9 + import HandleInput from "../components/login/HandleInput"; 9 10 10 11 interface LoginPageProps { 11 12 onSubmit: (handle: string) => void; ··· 23 24 const inputRef = useRef<HTMLInputElement>(null); 24 25 const [isSubmitting, setIsSubmitting] = useState(false); 25 26 const [strippedAtMessage, setStrippedAtMessage] = useState(false); 27 + const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null); 26 28 27 29 const { fields, setValue, validate, getFieldProps } = useFormValidation({ 28 30 handle: "", 29 31 }); 30 32 31 - // Sync typeahead selection with form state 33 + // Sync typeahead selection with form state and extract avatar 32 34 useEffect(() => { 33 35 const input = inputRef.current; 34 36 if (!input) return; ··· 48 50 } 49 51 } 50 52 53 + // Check if typeahead has selection data (avatar) 54 + const typeaheadElement = input.closest("actor-typeahead"); 55 + if (typeaheadElement) { 56 + const avatar = typeaheadElement.getAttribute("data-avatar"); 57 + if (avatar) { 58 + setSelectedAvatar(avatar); 59 + } else if (value === "") { 60 + // Clear avatar when input is cleared 61 + setSelectedAvatar(null); 62 + } 63 + } 64 + 51 65 // Update form state 52 66 setValue("handle", value); 53 67 }; ··· 57 71 input.addEventListener("change", handleInputChange); 58 72 input.addEventListener("blur", handleInputChange); 59 73 74 + // Also listen for custom typeahead selection event if it exists 75 + const handleSelection = (e: Event) => { 76 + const customEvent = e as CustomEvent; 77 + if (customEvent.detail?.avatar) { 78 + setSelectedAvatar(customEvent.detail.avatar); 79 + } 80 + }; 81 + input.addEventListener("actor-select", handleSelection as EventListener); 82 + 60 83 return () => { 61 84 input.removeEventListener("input", handleInputChange); 62 85 input.removeEventListener("change", handleInputChange); 63 86 input.removeEventListener("blur", handleInputChange); 87 + input.removeEventListener("actor-select", handleSelection as EventListener); 64 88 }; 65 89 }, [setValue, strippedAtMessage]); 66 90 ··· 137 161 > 138 162 <div> 139 163 <actor-typeahead rows={5}> 140 - <input 164 + <HandleInput 141 165 ref={inputRef} 142 166 id="atproto-handle" 143 - type="text" 144 167 {...getFieldProps("handle")} 145 168 placeholder="username.bsky.social" 146 - className={`w-full px-4 py-3 bg-purple-50/50 dark:bg-slate-900/50 border-2 rounded-xl text-purple-900 dark:text-cyan-100 placeholder-purple-750/80 dark:placeholder-cyan-250/80 focus:outline-none focus:ring-2 transition-all ${ 147 - fields.handle.touched && fields.handle.error 148 - ? "border-red-500 focus:ring-red-500" 149 - : "border-cyan-500/50 dark:border-purple-500/30 focus:ring-orange-500 dark:focus:ring-amber-400 focus:border-transparent" 150 - }`} 169 + error={fields.handle.touched && !!fields.handle.error} 170 + selectedAvatar={selectedAvatar} 151 171 aria-required="true" 152 172 aria-invalid={ 153 173 fields.handle.touched && !!fields.handle.error