Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
190 lines
6.6 KiB
TypeScript
190 lines
6.6 KiB
TypeScript
|
|
"use client"
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { Button } from "@/components/common/button";
|
|
import { Input } from "@/components/common/input";
|
|
import { Label } from "@/components/common/label";
|
|
import { toast } from "sonner";
|
|
import { Loader2, ArrowRight } from "lucide-react";
|
|
import { motion } from "framer-motion";
|
|
|
|
export default function LoginForm() {
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [loginSuccess, setLoginSuccess] = useState(false);
|
|
const redirectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const redirectUrl = searchParams.get("redirectUrl") || "/dashboard";
|
|
|
|
// Check if already logged in
|
|
useEffect(() => {
|
|
const authToken = document.cookie
|
|
.split("; ")
|
|
.find((row) => row.startsWith("Authorization="))
|
|
?.split("=")[1];
|
|
|
|
if (authToken) {
|
|
router.push("/dashboard");
|
|
}
|
|
}, [router]);
|
|
|
|
// Cleanup timeout on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (redirectTimeoutRef.current) {
|
|
clearTimeout(redirectTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
async function handleLogin(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch("/api/auth/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
|
|
let data;
|
|
try {
|
|
data = await response.json();
|
|
} catch (parseError) {
|
|
console.error("Login parse error:", parseError);
|
|
toast.error("Server Error", {
|
|
description: "The server response couldn't be processed. Please try again."
|
|
});
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (response.ok && data.token) {
|
|
document.cookie = `Authorization=${data.token}; path=/; Secure; SameSite=Strict; max-age=10800`;
|
|
localStorage.setItem("Authorization", data.token);
|
|
|
|
toast.success("Welcome back!", { duration: 2000 });
|
|
setLoginSuccess(true);
|
|
|
|
router.push(redirectUrl);
|
|
redirectTimeoutRef.current = setTimeout(() => {
|
|
window.location.href = redirectUrl;
|
|
}, 1500);
|
|
} else {
|
|
const errorMessage = data.error || data.message || data.details || "Invalid credentials";
|
|
toast.error("Access Denied", {
|
|
description: errorMessage,
|
|
});
|
|
setIsLoading(false);
|
|
}
|
|
} catch (error) {
|
|
toast.error("Connection Error", {
|
|
description: "Unable to connect to server.",
|
|
});
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="w-full max-w-md relative z-10"
|
|
>
|
|
<div className={`
|
|
overflow-hidden rounded-2xl border border-white/10 shadow-2xl transition-all duration-300
|
|
bg-black/40 backdrop-blur-xl
|
|
p-8
|
|
${loginSuccess ? 'scale-[0.98] opacity-80' : 'scale-100 opacity-100'}
|
|
`}>
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-2xl font-bold text-white tracking-tight">Welcome back</h2>
|
|
<p className="mt-2 text-sm text-zinc-400">Enter your credentials to access the dashboard</p>
|
|
</div>
|
|
|
|
<form className="space-y-6" onSubmit={handleLogin}>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="username" className="text-zinc-300">Username</Label>
|
|
<Input
|
|
id="username"
|
|
type="text"
|
|
autoComplete="username"
|
|
required
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
className="bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-indigo-500/50 focus:ring-indigo-500/20 transition-all duration-300"
|
|
placeholder="Enter your username"
|
|
disabled={isLoading || loginSuccess}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="password" className="text-zinc-300">Password</Label>
|
|
<Link href="/auth/reset-password" className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors">
|
|
Forgot password?
|
|
</Link>
|
|
</div>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-indigo-500/50 focus:ring-indigo-500/20 transition-all duration-300"
|
|
placeholder="Enter your password"
|
|
disabled={isLoading || loginSuccess}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
className={`
|
|
w-full h-11 font-medium text-sm transition-all duration-300
|
|
${loginSuccess
|
|
? 'bg-green-500/90 hover:bg-green-500 text-white'
|
|
: 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-lg shadow-indigo-500/25'}
|
|
`}
|
|
disabled={isLoading || loginSuccess}
|
|
>
|
|
{isLoading ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Signing in...
|
|
</span>
|
|
) : loginSuccess ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<ArrowRight className="h-4 w-4" />
|
|
Redirecting...
|
|
</span>
|
|
) : (
|
|
"Sign in"
|
|
)}
|
|
</Button>
|
|
</form>
|
|
|
|
<div className="mt-8 pt-6 border-t border-white/10 text-center">
|
|
<p className="text-sm text-zinc-400">
|
|
Don't have an account?{" "}
|
|
<Link
|
|
href="/auth/register"
|
|
className="text-indigo-400 hover:text-indigo-300 font-medium transition-colors inline-flex items-center gap-1"
|
|
>
|
|
Sign up <ArrowRight className="w-3 h-3" />
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|