Created
January 20, 2026 10:16
-
-
Save SouravInsights/f2da3b7cdc14e8b063dc4afbfb6d58e0 to your computer and use it in GitHub Desktop.
The sign-in form of BeenThere
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState, useEffect, useRef, useCallback } from "react"; | |
| import { motion, useReducedMotion, AnimatePresence } from "framer-motion"; | |
| import { validateUsernameFormat } from "@/lib/utils/username-client"; | |
| import { debounce } from "lodash"; | |
| import { HiSparkles, HiArrowRight } from "react-icons/hi2"; | |
| import { HiMail } from "react-icons/hi"; | |
| type Props = { | |
| helperTextColor?: string; | |
| onSuccess?: (data: { | |
| username: string; | |
| displayUsername: string; | |
| email: string; | |
| position: number; | |
| inviteCode: string; | |
| }) => void; | |
| inviteCode?: string; | |
| }; | |
| type Step = "username" | "email"; | |
| type ValidationState = { | |
| isValid: boolean; | |
| isChecking: boolean; | |
| error: undefined | string; | |
| }; | |
| export function WaitlistClaimUsername({ | |
| helperTextColor, | |
| onSuccess, | |
| inviteCode, | |
| }: Props) { | |
| const [step, setStep] = useState<Step>("username"); | |
| const [username, setUsername] = useState(""); | |
| const [email, setEmail] = useState(""); | |
| const [usernameValidation, setUsernameValidation] = useState<ValidationState>( | |
| { | |
| isValid: false, | |
| isChecking: false, | |
| error: undefined, | |
| }, | |
| ); | |
| const [emailValidation, setEmailValidation] = useState<ValidationState>({ | |
| isValid: false, | |
| isChecking: false, | |
| error: undefined, | |
| }); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const [isFocused, setIsFocused] = useState(false); | |
| const prefersReduced = useReducedMotion(); | |
| // Store the current username in a ref so our async validation | |
| // can check if it's still relevant when it completes | |
| const usernameRef = useRef(username); | |
| usernameRef.current = username; | |
| const abortControllerRef = useRef<AbortController | null>(null); | |
| // Check username availability after the user stops typing | |
| const debouncedCheck = useCallback( | |
| debounce(async (name: string) => { | |
| if (!name.trim()) return; | |
| // Quick client-side format check first | |
| const formatResult = validateUsernameFormat(name); | |
| if (!formatResult.isValid) { | |
| setUsernameValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: formatResult.error, | |
| }); | |
| return; | |
| } | |
| // Now check if it's actually available | |
| setUsernameValidation((prev) => ({ | |
| ...prev, | |
| isChecking: true, | |
| error: undefined, | |
| })); | |
| // Cancel any pending request from a previous keystroke | |
| abortControllerRef.current?.abort(); | |
| abortControllerRef.current = new AbortController(); | |
| const signal = abortControllerRef.current.signal; | |
| try { | |
| const res = await fetch( | |
| `/api/waitlist/check-username?username=${encodeURIComponent(name)}`, | |
| { signal }, | |
| ); | |
| const data = await res.json().catch(() => ({})); | |
| // Make sure we're still checking the same username the user typed | |
| if (usernameRef.current !== name) return; | |
| if (res.ok && data.available) { | |
| setUsernameValidation({ | |
| isValid: true, | |
| isChecking: false, | |
| error: undefined, | |
| }); | |
| } else { | |
| setUsernameValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: data.error || "Username unavailable", | |
| }); | |
| } | |
| } catch (err: unknown) { | |
| if ((err as Error)?.name === "AbortError") return; | |
| if (usernameRef.current !== name) return; | |
| setUsernameValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: "Could not check username", | |
| }); | |
| } | |
| }, 500), | |
| [], | |
| ); | |
| // Trigger validation whenever the username changes | |
| useEffect(() => { | |
| // Clear the previous state while they're typing | |
| setUsernameValidation((prev) => ({ | |
| ...prev, | |
| isValid: false, | |
| error: undefined, | |
| isChecking: false, | |
| })); | |
| if (!username.trim()) { | |
| debouncedCheck.cancel(); | |
| return; | |
| } | |
| debouncedCheck(username); | |
| return () => { | |
| debouncedCheck.cancel(); | |
| }; | |
| }, [username, debouncedCheck]); | |
| const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| setUsername(e.target.value); | |
| }; | |
| const handleUsernameBlur = () => { | |
| setIsFocused(false); | |
| }; | |
| const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| setEmail(e.target.value); | |
| // Don't show "invalid email" while they're still typing | |
| // We don't want to show "Invalid Email" while they are typing "sourav@" | |
| setEmailValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: undefined, | |
| }); | |
| }; | |
| const handleEmailBlur = () => { | |
| setIsFocused(false); | |
| if (!email.trim()) { | |
| return; | |
| } | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!emailRegex.test(email)) { | |
| setEmailValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: "Please enter a valid email address", | |
| }); | |
| } else { | |
| setEmailValidation({ | |
| isValid: true, | |
| isChecking: false, | |
| error: undefined, | |
| }); | |
| } | |
| }; | |
| const handleUsernameSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| // If they hit enter while we're still checking, finish the check first | |
| debouncedCheck.flush(); | |
| if (!usernameValidation.isValid || usernameValidation.isChecking) return; | |
| setStep("email"); | |
| }; | |
| const handleFinalSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| // Validate email if they submitted without blurring the field first | |
| if (!emailValidation.isValid && !emailValidation.error) { | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!email.trim()) { | |
| return; | |
| } | |
| if (!emailRegex.test(email)) { | |
| setEmailValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: "Please enter a valid email address", | |
| }); | |
| return; | |
| } | |
| setEmailValidation({ | |
| isValid: true, | |
| isChecking: false, | |
| error: undefined, | |
| }); | |
| } else if (!emailValidation.isValid) { | |
| return; | |
| } | |
| if (isSubmitting) return; | |
| setIsSubmitting(true); | |
| setEmailValidation((prev) => ({ | |
| ...prev, | |
| error: undefined, | |
| isChecking: true, | |
| })); | |
| try { | |
| const res = await fetch("/api/waitlist/join", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: username.trim(), | |
| email: email.trim(), | |
| inviteCode: inviteCode || undefined, | |
| }), | |
| }); | |
| const result = await res.json(); | |
| if (res.ok && result.success) { | |
| onSuccess?.(result.data); | |
| // Redirect to public profile page with an URL parameter | |
| window.location.href = `/${result.data.username}?claimed=true`; | |
| } else { | |
| setEmailValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: result.error || "Failed to join waitlist", | |
| }); | |
| } | |
| } catch (err) { | |
| console.error("Network error:", err); | |
| setEmailValidation({ | |
| isValid: false, | |
| isChecking: false, | |
| error: "Something went wrong. Please try again.", | |
| }); | |
| } finally { | |
| setIsSubmitting(false); | |
| } | |
| }; | |
| const handleBackToUsername = () => { | |
| setStep("username"); | |
| setEmailValidation({ isValid: false, isChecking: false, error: undefined }); | |
| }; | |
| const isUsernameButtonEnabled = | |
| usernameValidation.isValid && !usernameValidation.isChecking; | |
| const isEmailButtonEnabled = emailValidation.isValid && !isSubmitting; | |
| // Shared button styles | |
| const enabledStyle: React.CSSProperties = { | |
| background: "linear-gradient(180deg, #A9DAB0 0%, #2D6C2F 100%)", | |
| borderRadius: 16, | |
| fontSize: 16, | |
| fontFamily: "var(--font-lora)", | |
| fontWeight: 600, | |
| color: "#fff", | |
| border: "none", | |
| minWidth: 120, | |
| height: 62, | |
| boxShadow: ` | |
| inset 0 -16px 0 rgba(45, 108, 47, 0.16), | |
| inset 0 -16px 0 rgba(45, 108, 47, 0.16), | |
| 0 4px 4px rgba(8, 8, 8, 0.24), | |
| 0 0 12px rgba(8, 8, 8, 0.24) | |
| `, | |
| }; | |
| const disabledStyle: React.CSSProperties = { | |
| background: "linear-gradient(180deg, #E6EFE6 0%, #D0D9CF 100%)", | |
| borderRadius: 16, | |
| fontSize: 16, | |
| fontFamily: "var(--font-lora)", | |
| fontWeight: 600, | |
| boxShadow: "none", | |
| minWidth: 120, | |
| height: 62, | |
| border: "1px solid rgba(0,0,0,0.06)", | |
| color: "#6B7280", | |
| }; | |
| const mobileEnabledStyle: React.CSSProperties = { | |
| ...enabledStyle, | |
| height: 52, | |
| minWidth: undefined, | |
| }; | |
| const mobileDisabledStyle: React.CSSProperties = { | |
| ...disabledStyle, | |
| height: 52, | |
| minWidth: undefined, | |
| }; | |
| const iconShadowStyle: React.CSSProperties = { | |
| filter: "drop-shadow(0px 2px 2px #00000066)", | |
| }; | |
| const labelShadowStyle: React.CSSProperties = { | |
| textShadow: "0px 2px 2px #00000066", | |
| }; | |
| return ( | |
| <motion.div | |
| initial={prefersReduced ? undefined : { opacity: 0, y: 8 }} | |
| animate={prefersReduced ? undefined : { opacity: 1, y: 0 }} | |
| transition={{ duration: 0.45, ease: "easeOut" }} | |
| className="w-full max-w-xl mx-auto px-4" | |
| > | |
| {/* Desktop / Tablet view */} | |
| <div className="hidden sm:block"> | |
| <AnimatePresence mode="wait"> | |
| {step === "username" && ( | |
| <motion.form | |
| key="username-form" | |
| initial={prefersReduced ? undefined : { opacity: 0, x: -20 }} | |
| animate={prefersReduced ? undefined : { opacity: 1, x: 0 }} | |
| exit={prefersReduced ? undefined : { opacity: 0, x: -20 }} | |
| transition={{ duration: 0.3, ease: "easeOut" }} | |
| onSubmit={handleUsernameSubmit} | |
| noValidate | |
| className="space-y-4" | |
| > | |
| <div | |
| className={`flex items-center bg-white border overflow-hidden transition-all duration-200 ${ | |
| isFocused | |
| ? "ring-4 ring-emerald-500/20 border-emerald-500" | |
| : "border-gray-200" | |
| }`} | |
| style={{ | |
| borderRadius: 20, | |
| boxShadow: isFocused | |
| ? "0px 4px 20px 0px rgba(16, 185, 129, 0.2)" | |
| : "0px 4px 16px 0px #0000003D", | |
| }} | |
| > | |
| <label | |
| htmlFor="username-input-desktop" | |
| className="pl-6 py-4 text-black text-xl font-medium flex-shrink-0 cursor-text" | |
| > | |
| beenthere.page/ | |
| </label> | |
| <input | |
| id="username-input-desktop" | |
| type="text" | |
| value={username} | |
| onChange={handleUsernameChange} | |
| onFocus={() => setIsFocused(true)} | |
| onBlur={handleUsernameBlur} | |
| placeholder="your-name" | |
| maxLength={20} | |
| required | |
| autoComplete="username" | |
| spellCheck={false} | |
| autoFocus | |
| aria-invalid={!!usernameValidation.error} | |
| aria-errormessage={ | |
| usernameValidation.error | |
| ? "username-error-desktop" | |
| : undefined | |
| } | |
| className="flex-1 p-2 text-black placeholder:text-gray-400 bg-transparent font-medium min-w-0" | |
| style={{ | |
| fontSize: "20px", | |
| fontFamily: "Inter, system-ui, -apple-system, sans-serif", | |
| fontWeight: 500, | |
| outline: "none", | |
| }} | |
| /> | |
| <div className="px-3 flex-shrink-0"> | |
| {usernameValidation.isChecking ? ( | |
| <div className="w-3 h-3 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" /> | |
| ) : username && usernameValidation.isValid ? ( | |
| <div className="w-2.5 h-2.5 bg-emerald-500 rounded-full shadow-sm" /> | |
| ) : username && usernameValidation.error ? ( | |
| <div className="w-2.5 h-2.5 bg-red-500 rounded-full shadow-sm" /> | |
| ) : null} | |
| </div> | |
| <div className="p-2 flex-shrink-0"> | |
| <button | |
| type="submit" | |
| disabled={!isUsernameButtonEnabled} | |
| className={`px-8 py-3 text-white font-semibold transition-all duration-200 flex items-center justify-center gap-2 ${ | |
| isUsernameButtonEnabled | |
| ? "hover:brightness-110 active:brightness-95" | |
| : "cursor-not-allowed" | |
| }`} | |
| style={ | |
| isUsernameButtonEnabled ? enabledStyle : disabledStyle | |
| } | |
| > | |
| <span | |
| style={ | |
| isUsernameButtonEnabled ? labelShadowStyle : undefined | |
| } | |
| > | |
| Next | |
| </span> | |
| <HiArrowRight | |
| className="w-4 h-4 flex-shrink-0" | |
| style={ | |
| isUsernameButtonEnabled ? iconShadowStyle : undefined | |
| } | |
| /> | |
| </button> | |
| </div> | |
| </div> | |
| </motion.form> | |
| )} | |
| {step === "email" && ( | |
| <motion.form | |
| key="email-form" | |
| initial={prefersReduced ? undefined : { opacity: 0, x: 20 }} | |
| animate={prefersReduced ? undefined : { opacity: 1, x: 0 }} | |
| exit={prefersReduced ? undefined : { opacity: 0, x: 20 }} | |
| transition={{ duration: 0.3, ease: "easeOut" }} | |
| onSubmit={handleFinalSubmit} | |
| noValidate | |
| className="space-y-4" | |
| > | |
| {/* Show what username they're claiming */} | |
| <div className="text-center mb-4"> | |
| <p | |
| className="text-sm text-gray-600 mb-1" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| Claiming username: | |
| </p> | |
| <div | |
| className="inline-flex items-center bg-gray-50 px-3 py-1.5 rounded-lg border border-gray-100" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| <span className="text-gray-600 text-sm font-medium"> | |
| beenthere.page/ | |
| </span> | |
| <span className="text-black text-sm font-semibold"> | |
| {username} | |
| </span> | |
| </div> | |
| </div> | |
| <div | |
| className={`flex items-center bg-white border overflow-hidden transition-all duration-200 ${ | |
| isFocused | |
| ? "ring-4 ring-emerald-500/20 border-emerald-500" | |
| : "border-gray-200" | |
| }`} | |
| style={{ | |
| borderRadius: 20, | |
| boxShadow: isFocused | |
| ? "0px 4px 20px 0px rgba(16, 185, 129, 0.2)" | |
| : "0px 4px 16px 0px #0000003D", | |
| }} | |
| > | |
| <button | |
| type="button" | |
| onClick={handleBackToUsername} | |
| className="pl-4 py-4 text-gray-400 hover:text-gray-600 transition-colors flex-shrink-0" | |
| aria-label="Back to username" | |
| > | |
| <HiArrowRight className="w-4 h-4 rotate-180" /> | |
| </button> | |
| <div className="px-3 py-4 flex items-center gap-3 flex-shrink-0"> | |
| <HiMail className="w-5 h-5 text-gray-400" /> | |
| <label | |
| htmlFor="email-input-desktop" | |
| className="text-gray-600 text-lg font-medium cursor-text" | |
| > | |
| Email: | |
| </label> | |
| </div> | |
| <input | |
| id="email-input-desktop" | |
| type="email" | |
| value={email} | |
| onChange={handleEmailChange} | |
| onFocus={() => setIsFocused(true)} | |
| onBlur={handleEmailBlur} | |
| placeholder="your@email.com" | |
| required | |
| autoComplete="email" | |
| autoFocus | |
| aria-invalid={!!emailValidation.error} | |
| aria-errormessage={ | |
| emailValidation.error ? "email-error-desktop" : undefined | |
| } | |
| className="flex-1 p-2 text-black placeholder:text-gray-400 bg-transparent font-medium min-w-0" | |
| style={{ | |
| fontSize: "20px", | |
| fontFamily: "Inter, system-ui, -apple-system, sans-serif", | |
| fontWeight: 500, | |
| outline: "none", | |
| }} | |
| /> | |
| <div className="px-3 flex-shrink-0"> | |
| {emailValidation.isChecking ? ( | |
| <div className="w-3 h-3 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" /> | |
| ) : email && emailValidation.isValid ? ( | |
| <div className="w-2.5 h-2.5 bg-emerald-500 rounded-full shadow-sm" /> | |
| ) : email && emailValidation.error ? ( | |
| <div className="w-2.5 h-2.5 bg-red-500 rounded-full shadow-sm" /> | |
| ) : null} | |
| </div> | |
| <div className="p-2 flex-shrink-0"> | |
| <button | |
| type="submit" | |
| disabled={!isEmailButtonEnabled} | |
| className={`px-8 py-3 text-white font-semibold transition-all duration-200 flex items-center justify-center gap-2 ${ | |
| isEmailButtonEnabled | |
| ? "hover:brightness-110 active:brightness-95" | |
| : "cursor-not-allowed" | |
| }`} | |
| style={isEmailButtonEnabled ? enabledStyle : disabledStyle} | |
| > | |
| <HiSparkles | |
| className="w-4 h-4 flex-shrink-0" | |
| style={isEmailButtonEnabled ? iconShadowStyle : undefined} | |
| /> | |
| <span | |
| style={ | |
| isEmailButtonEnabled ? labelShadowStyle : undefined | |
| } | |
| > | |
| {isSubmitting ? "Joining..." : "Claim"} | |
| </span> | |
| </button> | |
| </div> | |
| </div> | |
| </motion.form> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| {/* Mobile view */} | |
| <div className="block sm:hidden"> | |
| <AnimatePresence mode="wait"> | |
| {step === "username" && ( | |
| <motion.form | |
| key="username-form-mobile" | |
| initial={prefersReduced ? undefined : { opacity: 0, x: -20 }} | |
| animate={prefersReduced ? undefined : { opacity: 1, x: 0 }} | |
| exit={prefersReduced ? undefined : { opacity: 0, x: -20 }} | |
| transition={{ duration: 0.3, ease: "easeOut" }} | |
| onSubmit={handleUsernameSubmit} | |
| noValidate | |
| className="space-y-3" | |
| > | |
| <div | |
| className={`flex items-center bg-white/90 border transition-all duration-200 ${ | |
| isFocused | |
| ? "ring-4 ring-emerald-500/20 border-emerald-500" | |
| : "border-[rgba(255,255,255,0.12)]" | |
| }`} | |
| style={{ | |
| borderRadius: 16, | |
| boxShadow: isFocused | |
| ? "0px 4px 20px 0px rgba(16, 185, 129, 0.2)" | |
| : "0px 4px 16px 0px #0000003D", | |
| }} | |
| > | |
| <label | |
| htmlFor="username-input-mobile" | |
| className="pl-4 py-4 text-black text-sm font-medium flex-shrink-0" | |
| > | |
| beenthere.page/ | |
| </label> | |
| <input | |
| id="username-input-mobile" | |
| type="text" | |
| value={username} | |
| onChange={handleUsernameChange} | |
| onFocus={() => setIsFocused(true)} | |
| onBlur={handleUsernameBlur} | |
| placeholder="your-name" | |
| maxLength={20} | |
| required | |
| autoComplete="username" | |
| spellCheck={false} | |
| autoFocus | |
| aria-invalid={!!usernameValidation.error} | |
| aria-errormessage={ | |
| usernameValidation.error | |
| ? "username-error-mobile" | |
| : undefined | |
| } | |
| className="flex-1 p-2 text-black placeholder:text-gray-400 bg-transparent focus:outline-none min-w-0" | |
| style={{ | |
| fontSize: "14px", | |
| fontFamily: "Inter, system-ui, -apple-system, sans-serif", | |
| fontWeight: 500, | |
| }} | |
| /> | |
| <div className="pl-1 pr-4 flex-shrink-0"> | |
| {usernameValidation.isChecking ? ( | |
| <div className="w-3 h-3 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" /> | |
| ) : username && usernameValidation.isValid ? ( | |
| <div className="w-2.5 h-2.5 bg-emerald-500 rounded-full shadow-sm" /> | |
| ) : username && usernameValidation.error ? ( | |
| <div className="w-2.5 h-2.5 bg-red-500 rounded-full shadow-sm" /> | |
| ) : null} | |
| </div> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={!isUsernameButtonEnabled} | |
| className={`w-full px-6 py-4 font-semibold transition-all duration-200 flex items-center justify-center gap-2 ${ | |
| isUsernameButtonEnabled | |
| ? "hover:brightness-110 active:brightness-95" | |
| : "cursor-not-allowed" | |
| }`} | |
| style={ | |
| isUsernameButtonEnabled | |
| ? mobileEnabledStyle | |
| : mobileDisabledStyle | |
| } | |
| > | |
| <HiArrowRight | |
| className="w-4 h-4 flex-shrink-0" | |
| style={isUsernameButtonEnabled ? iconShadowStyle : undefined} | |
| /> | |
| <span | |
| style={isUsernameButtonEnabled ? labelShadowStyle : undefined} | |
| > | |
| Next | |
| </span> | |
| </button> | |
| </motion.form> | |
| )} | |
| {step === "email" && ( | |
| <motion.form | |
| key="email-form-mobile" | |
| initial={prefersReduced ? undefined : { opacity: 0, x: 20 }} | |
| animate={prefersReduced ? undefined : { opacity: 1, x: 0 }} | |
| exit={prefersReduced ? undefined : { opacity: 0, x: 20 }} | |
| transition={{ duration: 0.3, ease: "easeOut" }} | |
| onSubmit={handleFinalSubmit} | |
| noValidate | |
| className="space-y-3" | |
| > | |
| <button | |
| type="button" | |
| onClick={handleBackToUsername} | |
| className="text-sm text-gray-400 hover:text-gray-600 transition-colors mb-2" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| ← beenthere.page/ | |
| <span className="text-black font-medium">{username}</span> | |
| </button> | |
| <div | |
| className={`flex items-center bg-white/90 border transition-all duration-200 ${ | |
| isFocused | |
| ? "ring-4 ring-emerald-500/20 border-emerald-500" | |
| : "border-[rgba(255,255,255,0.12)]" | |
| }`} | |
| style={{ | |
| borderRadius: 16, | |
| boxShadow: isFocused | |
| ? "0px 4px 20px 0px rgba(16, 185, 129, 0.2)" | |
| : "0px 4px 16px 0px #0000003D", | |
| }} | |
| > | |
| <HiMail className="ml-4 w-4 h-4 text-gray-400 flex-shrink-0" /> | |
| <input | |
| id="email-input-mobile" | |
| aria-label="Enter email address" | |
| type="email" | |
| value={email} | |
| onChange={handleEmailChange} | |
| onFocus={() => setIsFocused(true)} | |
| onBlur={handleEmailBlur} | |
| placeholder="your@email.com" | |
| required | |
| autoComplete="email" | |
| autoFocus | |
| aria-invalid={!!emailValidation.error} | |
| aria-errormessage={ | |
| emailValidation.error ? "email-error-mobile" : undefined | |
| } | |
| className="flex-1 py-4 ml-2 text-black placeholder:text-gray-400 bg-transparent focus:outline-none min-w-0" | |
| style={{ | |
| fontSize: "14px", | |
| fontFamily: "Inter, system-ui, -apple-system, sans-serif", | |
| fontWeight: 500, | |
| }} | |
| /> | |
| <div className="pl-1 pr-4 flex-shrink-0"> | |
| {emailValidation.isChecking ? ( | |
| <div className="w-3 h-3 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" /> | |
| ) : email && emailValidation.isValid ? ( | |
| <div className="w-2.5 h-2.5 bg-emerald-500 rounded-full shadow-sm" /> | |
| ) : email && emailValidation.error ? ( | |
| <div className="w-2.5 h-2.5 bg-red-500 rounded-full shadow-sm" /> | |
| ) : null} | |
| </div> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={!isEmailButtonEnabled} | |
| className={`w-full px-6 py-4 font-semibold transition-all duration-200 flex items-center justify-center gap-2 ${ | |
| isEmailButtonEnabled | |
| ? "hover:brightness-110 active:brightness-95" | |
| : "cursor-not-allowed" | |
| }`} | |
| style={ | |
| isEmailButtonEnabled | |
| ? mobileEnabledStyle | |
| : mobileDisabledStyle | |
| } | |
| > | |
| <HiSparkles | |
| className="w-4 h-4 flex-shrink-0" | |
| style={isEmailButtonEnabled ? iconShadowStyle : undefined} | |
| /> | |
| <span | |
| style={isEmailButtonEnabled ? labelShadowStyle : undefined} | |
| > | |
| {isSubmitting ? "Joining..." : "Claim"} | |
| </span> | |
| </button> | |
| </motion.form> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| {/* Validation feedback */} | |
| <div className="text-center min-h-[1.25rem] mt-4"> | |
| <div aria-live="polite" className="inline-block"> | |
| {step === "username" && ( | |
| <> | |
| {usernameValidation.error ? ( | |
| <motion.div | |
| id={ | |
| typeof window !== "undefined" && window.innerWidth < 640 | |
| ? "username-error-mobile" | |
| : "username-error-desktop" | |
| } | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="text-sm text-red-200 bg-red-900/20 px-3 py-1.5 rounded-md backdrop-blur-sm border border-red-300/20" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| {usernameValidation.error} | |
| </motion.div> | |
| ) : username && usernameValidation.isValid ? ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="text-sm text-emerald-200 bg-emerald-900/20 px-3 py-1.5 rounded-md backdrop-blur-sm border border-emerald-300/20" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| Available ✓ | |
| </motion.div> | |
| ) : ( | |
| <div | |
| className="text-sm" | |
| style={{ | |
| fontFamily: "var(--font-inter)", | |
| color: helperTextColor, | |
| }} | |
| > | |
| 3-20 characters, letters and hyphens only | |
| </div> | |
| )} | |
| </> | |
| )} | |
| {step === "email" && ( | |
| <> | |
| {emailValidation.error ? ( | |
| <motion.div | |
| id={ | |
| typeof window !== "undefined" && window.innerWidth < 640 | |
| ? "email-error-mobile" | |
| : "email-error-desktop" | |
| } | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="text-sm text-red-200 bg-red-900/20 px-3 py-1.5 rounded-md backdrop-blur-sm border border-red-300/20" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| {emailValidation.error} | |
| </motion.div> | |
| ) : email && emailValidation.isValid ? ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="text-sm text-emerald-200 bg-emerald-900/20 px-3 py-1.5 rounded-md backdrop-blur-sm border border-emerald-300/20" | |
| style={{ fontFamily: "var(--font-inter)" }} | |
| > | |
| Ready to claim ✓ | |
| </motion.div> | |
| ) : ( | |
| <div | |
| className="text-sm" | |
| style={{ | |
| fontFamily: "var(--font-inter)", | |
| color: helperTextColor, | |
| }} | |
| > | |
| {`We'll secure your username with this email`} | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| </motion.div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment