AI / src /components /AuthModal.tsx
Aura-Studio's picture
Update src/components/AuthModal.tsx
d33498f verified
Raw History Blame Contribute Delete
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>
);
}