Overhaul the GuruRMM dashboard with a dark cyberpunk aesthetic featuring glassmorphism effects, cyan accent lighting, and smooth animations. Visual Changes: - Dark theme with CSS variables for consistent theming - Glassmorphism card effects with colored glow variants - Grid pattern backgrounds and floating geometric shapes - JetBrains Mono + Inter font pairing for tech aesthetic - Cyan, green, amber, and rose accent colors with glow effects Component Updates: - index.css: Complete CSS overhaul with utility classes, animations, and glassmorphism foundations (1300+ lines added) - Login.tsx: Glassmorphism login card with gradient logo and floating background shapes - Layout.tsx: Dark sidebar with cyan nav highlights, grid pattern main area, animated user profile section - Dashboard.tsx: Animated stat cards with staggered entrances, live status indicator with pulse animation, relative timestamps - Card.tsx: Added glow variants (cyan/green/amber/rose) with hover lift effects - Button.tsx: Gradient backgrounds, glow-on-hover, scale animations - Input.tsx: Dark styling with cyan focus glow, added Textarea component Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
233 lines
8.6 KiB
TypeScript
233 lines
8.6 KiB
TypeScript
import { useState, FormEvent } from "react";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { AxiosError } from "axios";
|
|
import { useAuth } from "../hooks/useAuth";
|
|
|
|
interface ApiErrorResponse {
|
|
error?: string;
|
|
message?: string;
|
|
}
|
|
|
|
/**
|
|
* Floating geometric shape component for background decoration
|
|
*/
|
|
function FloatingShape({
|
|
className,
|
|
delay = 0,
|
|
size = 40
|
|
}: {
|
|
className?: string;
|
|
delay?: number;
|
|
size?: number;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={`absolute opacity-20 ${className}`}
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
animationDelay: `${delay}s`,
|
|
}}
|
|
>
|
|
<div
|
|
className="w-full h-full border border-cyan-500/30 rotate-45 animate-float"
|
|
style={{ animationDelay: `${delay}s` }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Login() {
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await login(email, password);
|
|
navigate("/");
|
|
} catch (err) {
|
|
if (err instanceof AxiosError) {
|
|
const errorData = err.response?.data as ApiErrorResponse | undefined;
|
|
setError(errorData?.error || errorData?.message || err.message || "Login failed. Please try again.");
|
|
} else if (err instanceof Error) {
|
|
setError(err.message);
|
|
} else {
|
|
setError("An unexpected error occurred");
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[hsl(var(--bg-primary))]">
|
|
{/* Gradient background overlay */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-[hsl(var(--bg-primary))] via-[hsl(var(--bg-secondary))] to-[hsl(222_47%_8%)]" />
|
|
|
|
{/* Animated grid pattern */}
|
|
<div className="absolute inset-0 bg-grid-pattern opacity-30 animate-grid" />
|
|
|
|
{/* Radial gradient spotlight */}
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_hsl(var(--accent-cyan)/0.08)_0%,_transparent_70%)]" />
|
|
|
|
{/* Floating geometric shapes */}
|
|
<FloatingShape className="top-[10%] left-[10%]" delay={0} size={60} />
|
|
<FloatingShape className="top-[20%] right-[15%]" delay={1.5} size={40} />
|
|
<FloatingShape className="bottom-[30%] left-[8%]" delay={3} size={50} />
|
|
<FloatingShape className="bottom-[15%] right-[12%]" delay={2} size={35} />
|
|
<FloatingShape className="top-[50%] left-[5%]" delay={4} size={25} />
|
|
<FloatingShape className="top-[40%] right-[8%]" delay={2.5} size={45} />
|
|
|
|
{/* Login Card */}
|
|
<div className="relative z-10 w-full max-w-md mx-4 animate-fade-in-up">
|
|
<div className="glass rounded-2xl p-8 shadow-2xl">
|
|
{/* Logo and Branding */}
|
|
<div className="text-center mb-8">
|
|
{/* Logo Icon */}
|
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-cyan-500/20 to-teal-500/20 border border-cyan-500/30 mb-4 glow-cyan">
|
|
<svg
|
|
className="w-8 h-8 text-cyan-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={1.5}
|
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
|
|
{/* Title with gradient */}
|
|
<h1 className="text-3xl font-bold font-mono-display gradient-text-cyan-teal tracking-tight">
|
|
GuruRMM
|
|
</h1>
|
|
|
|
{/* Subtitle */}
|
|
<p className="mt-2 text-sm text-[hsl(var(--text-muted))] uppercase tracking-widest-custom font-mono-display">
|
|
Mission Control
|
|
</p>
|
|
</div>
|
|
|
|
{/* Login Form */}
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="p-4 rounded-lg bg-rose-950/50 border border-rose-500/30 animate-pulse-subtle">
|
|
<p className="text-sm text-rose-300 flex items-center gap-2">
|
|
<svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{error}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Email Field */}
|
|
<div className="space-y-2">
|
|
<label
|
|
htmlFor="email"
|
|
className="block text-xs font-medium text-[hsl(var(--text-secondary))] uppercase tracking-wider"
|
|
>
|
|
Email Address
|
|
</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
placeholder="operator@example.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
autoComplete="email"
|
|
className="w-full px-4 py-3 rounded-lg input-mission-control text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Password Field */}
|
|
<div className="space-y-2">
|
|
<label
|
|
htmlFor="password"
|
|
className="block text-xs font-medium text-[hsl(var(--text-secondary))] uppercase tracking-wider"
|
|
>
|
|
Password
|
|
</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
placeholder="Enter your password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
autoComplete="current-password"
|
|
className="w-full px-4 py-3 rounded-lg input-mission-control text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={`w-full py-3 px-4 rounded-lg btn-mission-control text-sm uppercase tracking-wider ${isLoading ? 'loading' : ''}`}
|
|
>
|
|
{isLoading ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
Authenticating...
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
|
</svg>
|
|
Access System
|
|
</span>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Divider */}
|
|
<div className="relative my-6">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-[hsl(var(--glass-border))]" />
|
|
</div>
|
|
<div className="relative flex justify-center text-xs">
|
|
<span className="px-3 bg-[hsl(var(--glass-bg))] text-[hsl(var(--text-muted))]">
|
|
OR
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Register Link */}
|
|
<p className="text-center text-sm text-[hsl(var(--text-secondary))]">
|
|
New operator?{" "}
|
|
<Link
|
|
to="/register"
|
|
className="text-cyan-400 hover:text-cyan-300 transition-colors font-medium hover:underline underline-offset-4"
|
|
>
|
|
Request Access
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Footer tagline */}
|
|
<p className="text-center mt-6 text-xs text-[hsl(var(--text-muted))] font-mono-display">
|
|
Secure Remote Management Infrastructure
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|