+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
import HeroSection from "../components/login/HeroSection";
7
import ValuePropsSection from "../components/login/ValuePropsSection";
8
import HowItWorksSection from "../components/login/HowItWorksSection";
9
10
interface LoginPageProps {
11
onSubmit: (handle: string) => void;
···
23
const inputRef = useRef<HTMLInputElement>(null);
24
const [isSubmitting, setIsSubmitting] = useState(false);
25
const [strippedAtMessage, setStrippedAtMessage] = useState(false);
26
27
const { fields, setValue, validate, getFieldProps } = useFormValidation({
28
handle: "",
29
});
30
31
-
// Sync typeahead selection with form state
32
useEffect(() => {
33
const input = inputRef.current;
34
if (!input) return;
···
48
}
49
}
50
51
// Update form state
52
setValue("handle", value);
53
};
···
57
input.addEventListener("change", handleInputChange);
58
input.addEventListener("blur", handleInputChange);
59
60
return () => {
61
input.removeEventListener("input", handleInputChange);
62
input.removeEventListener("change", handleInputChange);
63
input.removeEventListener("blur", handleInputChange);
64
};
65
}, [setValue, strippedAtMessage]);
66
···
137
>
138
<div>
139
<actor-typeahead rows={5}>
140
-
<input
141
ref={inputRef}
142
id="atproto-handle"
143
-
type="text"
144
{...getFieldProps("handle")}
145
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
-
}`}
151
aria-required="true"
152
aria-invalid={
153
fields.handle.touched && !!fields.handle.error
···
6
import HeroSection from "../components/login/HeroSection";
7
import ValuePropsSection from "../components/login/ValuePropsSection";
8
import HowItWorksSection from "../components/login/HowItWorksSection";
9
+
import HandleInput from "../components/login/HandleInput";
10
11
interface LoginPageProps {
12
onSubmit: (handle: string) => void;
···
24
const inputRef = useRef<HTMLInputElement>(null);
25
const [isSubmitting, setIsSubmitting] = useState(false);
26
const [strippedAtMessage, setStrippedAtMessage] = useState(false);
27
+
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null);
28
29
const { fields, setValue, validate, getFieldProps } = useFormValidation({
30
handle: "",
31
});
32
33
+
// Sync typeahead selection with form state and extract avatar
34
useEffect(() => {
35
const input = inputRef.current;
36
if (!input) return;
···
50
}
51
}
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
+
65
// Update form state
66
setValue("handle", value);
67
};
···
71
input.addEventListener("change", handleInputChange);
72
input.addEventListener("blur", handleInputChange);
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
+
83
return () => {
84
input.removeEventListener("input", handleInputChange);
85
input.removeEventListener("change", handleInputChange);
86
input.removeEventListener("blur", handleInputChange);
87
+
input.removeEventListener("actor-select", handleSelection as EventListener);
88
};
89
}, [setValue, strippedAtMessage]);
90
···
161
>
162
<div>
163
<actor-typeahead rows={5}>
164
+
<HandleInput
165
ref={inputRef}
166
id="atproto-handle"
167
{...getFieldProps("handle")}
168
placeholder="username.bsky.social"
169
+
error={fields.handle.touched && !!fields.handle.error}
170
+
selectedAvatar={selectedAvatar}
171
aria-required="true"
172
aria-invalid={
173
fields.handle.touched && !!fields.handle.error