Spaces:
Running
Running
Download src/components/AuthModal.tsx from Aura-Studio/AI: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/components/AuthModal.tsx
- Command line
-
hf download hf://spaces/Aura-Studio/AI/src/components/AuthModal.tsx
-
curl -L -o AuthModal.tsx https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/components/AuthModal.tsx
17.1 kB
| import React, { useState, useEffect, useRef } from "react"; | |
| import { | |
| Lock, | |
| User, | |
| KeyRound, | |
| ShieldCheck, | |
| HardDrive, | |
| CheckCircle2, | |
| AlertCircle, | |
| X, | |
| Eye, | |
| EyeOff, | |
| ArrowRight, | |
| Sparkles, | |
| FolderLock, | |
| Globe, | |
| Loader2 | |
| } from "lucide-react"; | |
| interface AuthModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onAuthSuccess: (user: { | |
| id: string; | |
| username: string; | |
| createdAt: string; | |
| stats: any; | |
| }, cloudData?: any) => void; | |
| initialMode?: "signin" | "signup"; | |
| } | |
| export default function AuthModal({ | |
| isOpen, | |
| onClose, | |
| onAuthSuccess, | |
| initialMode = "signin" | |
| }: AuthModalProps) { | |
| const [mode, setMode] = useState<"signin" | "signup">(initialMode); | |
| const [username, setUsername] = useState(""); | |
| const [password, setPassword] = useState(""); | |
| const [confirmPassword, setConfirmPassword] = useState(""); | |
| const [showPassword, setShowPassword] = useState(false); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [successMsg, setSuccessMsg] = useState<string | null>(null); | |
| // Real-time username availability checking | |
| const [usernameStatus, setUsernameStatus] = useState<{ | |
| checking: boolean; | |
| available?: boolean; | |
| exists?: boolean; | |
| message?: string; | |
| }>({ checking: false }); | |
| const checkTimeoutRef = useRef<any>(null); | |
| // Sync initialMode when modal opens | |
| useEffect(() => { | |
| if (isOpen) { | |
| setMode(initialMode); | |
| setError(null); | |
| setSuccessMsg(null); | |
| } | |
| }, [isOpen, initialMode]); | |
| // Debounced username availability check | |
| useEffect(() => { | |
| const trimmed = username.trim(); | |
| if (!trimmed || trimmed.length < 3) { | |
| setUsernameStatus({ checking: false }); | |
| return; | |
| } | |
| if (checkTimeoutRef.current) { | |
| clearTimeout(checkTimeoutRef.current); | |
| } | |
| setUsernameStatus({ checking: true }); | |
| checkTimeoutRef.current = setTimeout(async () => { | |
| try { | |
| const res = await fetch(`/api/auth/check-username?username=${encodeURIComponent(trimmed)}`); | |
| const data = await res.json(); | |
| setUsernameStatus({ | |
| checking: false, | |
| available: data.available, | |
| exists: data.exists, | |
| message: data.message | |
| }); | |
| } catch (e) { | |
| setUsernameStatus({ checking: false }); | |
| } | |
| }, 300); | |
| return () => { | |
| if (checkTimeoutRef.current) clearTimeout(checkTimeoutRef.current); | |
| }; | |
| }, [username, mode]); | |
| if (!isOpen) return null; | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setError(null); | |
| setSuccessMsg(null); | |
| const trimmedUser = username.trim(); | |
| if (!trimmedUser) { | |
| setError("Please enter your username."); | |
| return; | |
| } | |
| if (!password) { | |
| setError("Please enter your password."); | |
| return; | |
| } | |
| if (mode === "signup") { | |
| if (password.length < 6) { | |
| setError("Password must be at least 6 characters long."); | |
| return; | |
| } | |
| if (password !== confirmPassword) { | |
| setError("Passwords do not match. Please verify your confirm password."); | |
| return; | |
| } | |
| } | |
| setLoading(true); | |
| try { | |
| const endpoint = mode === "signup" ? "/api/auth/register" : "/api/auth/login"; | |
| const response = await fetch(endpoint, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: trimmedUser, | |
| password | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok || !data.success) { | |
| throw new Error(data.error || "Authentication failed."); | |
| } | |
| setSuccessMsg( | |
| mode === "signup" | |
| ? `Welcome, @${data.user.username}! Your cross-device account is ready.` | |
| : `Signed in as @${data.user.username}. Restoring your cloud chats...` | |
| ); | |
| // Instantly apply user credentials and hydrate chats across devices | |
| onAuthSuccess(data.user, data.data); | |
| setTimeout(() => { | |
| onClose(); | |
| }, 500); | |
| } catch (err: any) { | |
| setError(err.message || "An unexpected authentication error occurred."); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-fade-in font-sans"> | |
| <div | |
| className="relative w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| {/* Top Gradient Banner */} | |
| <div className="bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 p-6 text-white relative"> | |
| <button | |
| onClick={onClose} | |
| className="absolute top-4 right-4 h-8 w-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center transition-colors cursor-pointer" | |
| aria-label="Close" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| <div className="flex items-center gap-3 mb-2"> | |
| <div className="h-10 w-10 rounded-2xl bg-white/20 backdrop-blur flex items-center justify-center shadow-inner"> | |
| <FolderLock className="h-5 w-5 text-white" /> | |
| </div> | |
| <div> | |
| <h3 className="text-lg font-bold"> | |
| {mode === "signin" ? "Sign In to AI Assistant" : "Create Aura Store Account"} | |
| </h3> | |
| <p className="text-xs text-white/80"> | |
| {mode === "signin" ? "Available on any device with 1 GB Cloud Storage" : "Claim your username across all your devices"} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="mt-3 flex items-center justify-between text-[11px] bg-black/20 px-3 py-1.5 rounded-xl border border-white/10"> | |
| <div className="flex items-center gap-1.5"> | |
| <Globe className="h-3.5 w-3.5 text-pink-200 shrink-0" /> | |
| <span className="text-white/90">Cross-Device Cloud Sign In</span> | |
| </div> | |
| <span className="font-mono text-pink-200 text-[10px]">1 GB Storage</span> | |
| </div> | |
| </div> | |
| {/* Auth Mode Toggle Tabs */} | |
| <div className="flex border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/50 p-1.5"> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setMode("signin"); | |
| setError(null); | |
| }} | |
| className={`flex-1 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${ | |
| mode === "signin" | |
| ? "bg-white dark:bg-slate-800 text-indigo-600 dark:text-indigo-400 shadow-sm" | |
| : "text-slate-500 hover:text-slate-800 dark:hover:text-slate-300" | |
| }`} | |
| > | |
| Sign In | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setMode("signup"); | |
| setError(null); | |
| }} | |
| className={`flex-1 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${ | |
| mode === "signup" | |
| ? "bg-white dark:bg-slate-800 text-indigo-600 dark:text-indigo-400 shadow-sm" | |
| : "text-slate-500 hover:text-slate-800 dark:hover:text-slate-300" | |
| }`} | |
| > | |
| Create Account | |
| </button> | |
| </div> | |
| {/* Main Form */} | |
| <form onSubmit={handleSubmit} className="p-6 space-y-4"> | |
| {error && ( | |
| <div className="p-3 rounded-xl bg-red-50 dark:bg-red-950/50 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 text-xs flex items-start gap-2"> | |
| <AlertCircle className="h-4 w-4 shrink-0 mt-0.5" /> | |
| <span>{error}</span> | |
| </div> | |
| )} | |
| {successMsg && ( | |
| <div className="p-3 rounded-xl bg-emerald-50 dark:bg-emerald-950/50 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 text-xs flex items-start gap-2"> | |
| <CheckCircle2 className="h-4 w-4 shrink-0 mt-0.5" /> | |
| <span>{successMsg}</span> | |
| </div> | |
| )} | |
| {/* Username Field with Live Availability Indicator */} | |
| <div> | |
| <div className="flex items-center justify-between mb-1"> | |
| <label className="block text-xs font-bold text-slate-700 dark:text-slate-300"> | |
| Username | |
| </label> | |
| {username.trim().length >= 3 && ( | |
| <div className="text-[11px] flex items-center gap-1"> | |
| {usernameStatus.checking ? ( | |
| <span className="text-slate-400 flex items-center gap-1"> | |
| <Loader2 className="h-3 w-3 animate-spin" /> Checking... | |
| </span> | |
| ) : mode === "signup" ? ( | |
| usernameStatus.available ? ( | |
| <span className="text-emerald-600 dark:text-emerald-400 font-semibold flex items-center gap-1"> | |
| <CheckCircle2 className="h-3 w-3" /> Available | |
| </span> | |
| ) : usernameStatus.exists ? ( | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setMode("signin"); | |
| setError(null); | |
| }} | |
| className="text-indigo-600 dark:text-indigo-400 font-bold hover:underline" | |
| > | |
| Taken (Sign In) | |
| </button> | |
| ) : null | |
| ) : ( | |
| usernameStatus.exists ? ( | |
| <span className="text-emerald-600 dark:text-emerald-400 font-semibold flex items-center gap-1"> | |
| <CheckCircle2 className="h-3 w-3" /> Account Found | |
| </span> | |
| ) : null | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| <div className="relative"> | |
| <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400"> | |
| <User className="h-4 w-4" /> | |
| </div> | |
| <input | |
| id="auth-username" | |
| type="text" | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| placeholder="e.g. harsha, hari, alex" | |
| autoComplete="username" | |
| required | |
| className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 text-slate-900 dark:text-white text-xs focus:ring-2 focus:ring-indigo-500 focus:outline-none" | |
| /> | |
| </div> | |
| <p className="mt-1 text-[10px] text-slate-500 dark:text-slate-400"> | |
| {mode === "signup" | |
| ? "Usernames are universal and can be signed into from any phone, laptop, or browser." | |
| : "Enter your registered username to load your cloud chats and 1 GB storage."} | |
| </p> | |
| </div> | |
| {/* Password Field */} | |
| <div> | |
| <label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1"> | |
| Password | |
| </label> | |
| <div className="relative"> | |
| <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400"> | |
| <KeyRound className="h-4 w-4" /> | |
| </div> | |
| <input | |
| id="auth-password" | |
| type={showPassword ? "text" : "password"} | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| placeholder="Enter password (min 6 characters)" | |
| autoComplete={mode === "signin" ? "current-password" : "new-password"} | |
| required | |
| className="w-full pl-9 pr-10 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 text-slate-900 dark:text-white text-xs focus:ring-2 focus:ring-indigo-500 focus:outline-none" | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => setShowPassword(!showPassword)} | |
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer" | |
| > | |
| {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | |
| </button> | |
| </div> | |
| </div> | |
| {/* Confirm Password Field (Only for Create Account) */} | |
| {mode === "signup" && ( | |
| <div> | |
| <label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1"> | |
| Confirm Password | |
| </label> | |
| <div className="relative"> | |
| <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400"> | |
| <ShieldCheck className="h-4 w-4" /> | |
| </div> | |
| <input | |
| id="auth-confirm-password" | |
| type={showPassword ? "text" : "password"} | |
| value={confirmPassword} | |
| onChange={(e) => setConfirmPassword(e.target.value)} | |
| placeholder="Re-enter password to confirm" | |
| autoComplete="new-password" | |
| required | |
| className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 text-slate-900 dark:text-white text-xs focus:ring-2 focus:ring-indigo-500 focus:outline-none" | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| {/* Account Storage Features Info Box */} | |
| <div className="p-3 bg-slate-50 dark:bg-slate-800/60 rounded-2xl border border-slate-100 dark:border-slate-800 space-y-1.5"> | |
| <div className="flex items-center justify-between text-[11px] font-bold text-slate-700 dark:text-slate-300"> | |
| <span className="flex items-center gap-1.5"> | |
| <HardDrive className="h-3.5 w-3.5 text-indigo-500" /> | |
| Aura Store Quota: | |
| </span> | |
| <span className="text-indigo-600 dark:text-indigo-400 font-mono">1.00 GB Cloud</span> | |
| </div> | |
| <p className="text-[10px] text-slate-500 dark:text-slate-400 leading-relaxed"> | |
| Auto-creates 4 dedicated cloud folders: <span className="font-semibold text-slate-700 dark:text-slate-300">media storage</span>, <span className="font-semibold text-slate-700 dark:text-slate-300">chats</span>, <span className="font-semibold text-slate-700 dark:text-slate-300">generated media</span>, and <span className="font-semibold text-slate-700 dark:text-slate-300">pinned chats</span>. | |
| </p> | |
| </div> | |
| {/* Submit Action Button */} | |
| <button | |
| type="submit" | |
| disabled={loading} | |
| className="w-full py-3 px-4 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-bold text-xs flex items-center justify-center gap-2 shadow-lg shadow-indigo-500/25 transition-all cursor-pointer disabled:opacity-50" | |
| > | |
| {loading ? ( | |
| <div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> | |
| ) : ( | |
| <> | |
| <span>{mode === "signin" ? "Sign In on this Device" : "Create Account & Get 1 GB"}</span> | |
| <ArrowRight className="h-4 w-4" /> | |
| </> | |
| )} | |
| </button> | |
| {/* Switch Mode Clickable Text */} | |
| <div className="text-center pt-2"> | |
| {mode === "signin" ? ( | |
| <p className="text-xs text-slate-500 dark:text-slate-400"> | |
| Don't have an account?{" "} | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setMode("signup"); | |
| setError(null); | |
| }} | |
| className="font-bold text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer" | |
| > | |
| Create account | |
| </button> | |
| </p> | |
| ) : ( | |
| <p className="text-xs text-slate-500 dark:text-slate-400"> | |
| Already have an account?{" "} | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setMode("signin"); | |
| setError(null); | |
| }} | |
| className="font-bold text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer" | |
| > | |
| Sign in | |
| </button> | |
| </p> | |
| )} | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| ); | |
| } | |