Files
claudetools/projects/msp-tools/guru-rmm/dashboard/src/pages/Login.tsx
azcomputerguru 88ed39a1b8 feat(dashboard): Complete "Mission Control" UI redesign
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>
2026-01-21 06:23:59 -07:00

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>
);
}