+56
src/components/login/HandleInput.tsx
+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
+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