Files
ember-market-frontend/app/auth/login/components/LoginForm.tsx
NotII 2c48ecd2b4 Add product applicability controls to promotion forms
Introduces product selection and exclusion controls to both new and edit promotion forms, allowing promotions to target all, specific, or all-but-specific products. Adds a reusable ProductSelector component, updates promotion types to support new fields, and adjusts cookie max-age for authentication. Also adds two new business quotes.
2025-08-07 16:05:31 +01:00

172 lines
6.2 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/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
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 {
// Using fetch directly with the proxy path
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) {
// Store the token in both cookie and localStorage for redundancy
document.cookie = `Authorization=${data.token}; path=/; Secure; SameSite=Strict; max-age=10800`;
localStorage.setItem("Authorization", data.token);
// Show success notification
toast.success("Login successful");
// Set success state for animation
setLoginSuccess(true);
// Try Next.js router navigation
router.push(redirectUrl);
// Set up a fallback manual redirect if Next.js navigation doesn't work
redirectTimeoutRef.current = setTimeout(() => {
window.location.href = redirectUrl;
}, 1500); // Wait 1.5 seconds before trying manual redirect
} else {
// Handle HTTP error responses
const errorMessage = data.error || data.message || data.details || "Invalid credentials";
toast.error("Login Failed", {
description: errorMessage,
});
console.error("Login error response:", { status: response.status, data });
setIsLoading(false);
}
} catch (error) {
toast.error("Connection Error", {
description: "Unable to connect to the server. Please check your internet connection and try again.",
});
console.error("Login network error:", error);
setIsLoading(false);
}
}
return (
<div className={`flex items-center justify-center min-h-screen bg-gray-100 dark:bg-[#0F0F12] transition-opacity duration-300 ${loginSuccess ? 'opacity-0' : 'opacity-100'}`}>
<div className={`w-full max-w-md p-8 space-y-8 bg-white dark:bg-[#1F1F23] rounded-xl shadow-lg transition-all duration-300 ${loginSuccess ? 'scale-95 opacity-0' : 'scale-100 opacity-100'} ${isLoading && !loginSuccess ? 'animate-pulse' : ''}`}>
<div className="text-center">
<h2 className="mt-6 text-3xl font-bold text-gray-900 dark:text-white">Welcome back</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">Please sign in to your account</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
<div className="space-y-4">
<div className="animate-in fade-in duration-500">
<Label htmlFor="username">Username</Label>
<Input
id="username"
name="username"
type="text"
autoComplete="username"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
className="mt-1"
disabled={isLoading || loginSuccess}
/>
</div>
<div className="animate-in fade-in duration-500 delay-150">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1"
disabled={isLoading || loginSuccess}
/>
</div>
</div>
<Button
type="submit"
className={`w-full animate-in fade-in-50 duration-500 delay-300 ${loginSuccess ? 'bg-green-600 hover:bg-green-700' : ''}`}
disabled={isLoading || loginSuccess}
>
{isLoading ? (
<span className="flex items-center justify-center">
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Signing in...
</span>
) : loginSuccess ? (
<span className="flex items-center justify-center">
Redirecting...
</span>
) : (
"Sign in"
)}
</Button>
</form>
<p className="mt-10 text-sm text-center text-gray-600 dark:text-gray-400 animate-in fade-in duration-500 delay-500">
Don't have an account?{" "}
<Link href="/auth/register" className="text-blue-600 hover:underline dark:text-blue-400">
Sign up
</Link>
</p>
</div>
</div>
);
}