Add JWT authentication with animated login page

- Backend: JWT auth with /api/auth/login and /api/auth/verify endpoints
- Middleware: requireAuth protects all /api routes except /api/auth
- Frontend: Animated characters login page with eye-tracking effects
- Auth state persisted in localStorage with token verification on mount
- Sidebar logout button, 401 auto-logout, 30-day token expiry
- shadcn button, input, checkbox, label components added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 16:35:30 +05:30
parent eb6c097664
commit ed13991515
15 changed files with 1015 additions and 16 deletions

View File

@@ -15,6 +15,9 @@
"vite": "^8.0.1"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.95.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -25,7 +28,7 @@
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.5.0",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"
}
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"
import { useEffect, useState, useCallback } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { Cpu, MemoryStick, HardDrive, Container } from "lucide-react"
import { Sidebar } from "@/components/layout/Sidebar"
@@ -14,10 +14,25 @@ import { ContainersPage } from "@/components/pages/ContainersPage"
import { MemoryPage } from "@/components/pages/MemoryPage"
import { StoragePage } from "@/components/pages/StoragePage"
import { NetworkPage } from "@/components/pages/NetworkPage"
import { LoginPage } from "@/components/ui/animated-characters-login-page"
import { useServerHealth } from "@/hooks/useServerHealth"
import { useCpuHistory } from "@/hooks/useCpuHistory"
import { formatBytes } from "@/lib/utils"
const TOKEN_KEY = "eventify-auth-token"
function getStoredToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
function setStoredToken(token: string) {
localStorage.setItem(TOKEN_KEY, token)
}
function clearStoredToken() {
localStorage.removeItem(TOKEN_KEY)
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -35,7 +50,7 @@ const PAGE_TITLES: Record<string, string> = {
network: "Network",
}
function DashboardContent({ activePage }: { activePage: string }) {
function DashboardContent({ activePage, onLogout }: { activePage: string; onLogout: () => void }) {
const {
data,
isLoading,
@@ -55,6 +70,13 @@ function DashboardContent({ activePage }: { activePage: string }) {
}
}, [data, addDataPoint])
// If we get a 401, the token is invalid — log out
useEffect(() => {
if (isError && error instanceof Error && error.message.includes("401")) {
onLogout()
}
}, [isError, error, onLogout])
const lastUpdated = dataUpdatedAt ? new Date(dataUpdatedAt).toISOString() : null
if (isError) {
@@ -76,7 +98,7 @@ function DashboardContent({ activePage }: { activePage: string }) {
</p>
<button
onClick={() => refetch()}
className="mt-3 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
className="mt-3 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 cursor-pointer"
>
Retry
</button>
@@ -229,13 +251,62 @@ function DashboardContent({ activePage }: { activePage: string }) {
}
export default function App() {
const [token, setToken] = useState<string | null>(getStoredToken)
const [activePage, setActivePage] = useState("dashboard")
const [verifying, setVerifying] = useState(true)
// Verify stored token on mount
useEffect(() => {
const stored = getStoredToken()
if (!stored) {
setVerifying(false)
return
}
fetch("/api/auth/verify", {
headers: { Authorization: `Bearer ${stored}` },
})
.then((res) => {
if (!res.ok) {
clearStoredToken()
setToken(null)
}
})
.catch(() => {
clearStoredToken()
setToken(null)
})
.finally(() => setVerifying(false))
}, [])
const handleLogin = useCallback((newToken: string) => {
setStoredToken(newToken)
setToken(newToken)
}, [])
const handleLogout = useCallback(() => {
clearStoredToken()
setToken(null)
queryClient.clear()
}, [])
if (verifying) {
return (
<div className="flex h-screen items-center justify-center bg-background">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
)
}
if (!token) {
return <LoginPage onLogin={handleLogin} />
}
return (
<QueryClientProvider client={queryClient}>
<div className="flex h-screen overflow-hidden">
<Sidebar activePage={activePage} onNavigate={setActivePage} />
<DashboardContent activePage={activePage} />
<Sidebar activePage={activePage} onNavigate={setActivePage} onLogout={handleLogout} />
<DashboardContent activePage={activePage} onLogout={handleLogout} />
</div>
</QueryClientProvider>
)

View File

@@ -7,6 +7,7 @@ import {
Settings,
HelpCircle,
Server,
LogOut,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
@@ -33,6 +34,7 @@ const bottomNav: NavItem[] = [
interface SidebarProps {
activePage: string
onNavigate: (page: string) => void
onLogout?: () => void
}
function NavButton({
@@ -61,7 +63,7 @@ function NavButton({
)
}
export function Sidebar({ activePage, onNavigate }: SidebarProps) {
export function Sidebar({ activePage, onNavigate, onLogout }: SidebarProps) {
return (
<aside className="flex h-screen w-60 shrink-0 flex-col border-r bg-sidebar-background">
{/* Brand */}
@@ -114,6 +116,17 @@ export function Sidebar({ activePage, onNavigate }: SidebarProps) {
All Systems Operational
</p>
</div>
{/* Logout */}
{onLogout && (
<button
onClick={onLogout}
className="mb-4 flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-red-50 hover:text-red-600 cursor-pointer"
>
<LogOut className="size-5 shrink-0" />
<span>Sign Out</span>
</button>
)}
</nav>
</aside>
)

View File

@@ -0,0 +1,560 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Eye, EyeOff, Sparkles } from "lucide-react";
interface PupilProps {
size?: number;
maxDistance?: number;
pupilColor?: string;
forceLookX?: number;
forceLookY?: number;
}
const Pupil = ({
size = 12,
maxDistance = 5,
pupilColor = "black",
forceLookX,
forceLookY
}: PupilProps) => {
const [mouseX, setMouseX] = useState<number>(0);
const [mouseY, setMouseY] = useState<number>(0);
const pupilRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMouseX(e.clientX);
setMouseY(e.clientY);
};
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
const calculatePupilPosition = () => {
if (!pupilRef.current) return { x: 0, y: 0 };
if (forceLookX !== undefined && forceLookY !== undefined) {
return { x: forceLookX, y: forceLookY };
}
const pupil = pupilRef.current.getBoundingClientRect();
const pupilCenterX = pupil.left + pupil.width / 2;
const pupilCenterY = pupil.top + pupil.height / 2;
const deltaX = mouseX - pupilCenterX;
const deltaY = mouseY - pupilCenterY;
const distance = Math.min(Math.sqrt(deltaX ** 2 + deltaY ** 2), maxDistance);
const angle = Math.atan2(deltaY, deltaX);
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
return { x, y };
};
const pupilPosition = calculatePupilPosition();
return (
<div
ref={pupilRef}
className="rounded-full"
style={{
width: `${size}px`,
height: `${size}px`,
backgroundColor: pupilColor,
transform: `translate(${pupilPosition.x}px, ${pupilPosition.y}px)`,
transition: 'transform 0.1s ease-out',
}}
/>
);
};
interface EyeBallProps {
size?: number;
pupilSize?: number;
maxDistance?: number;
eyeColor?: string;
pupilColor?: string;
isBlinking?: boolean;
forceLookX?: number;
forceLookY?: number;
}
const EyeBall = ({
size = 48,
pupilSize = 16,
maxDistance = 10,
eyeColor = "white",
pupilColor = "black",
isBlinking = false,
forceLookX,
forceLookY
}: EyeBallProps) => {
const [mouseX, setMouseX] = useState<number>(0);
const [mouseY, setMouseY] = useState<number>(0);
const eyeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMouseX(e.clientX);
setMouseY(e.clientY);
};
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
const calculatePupilPosition = () => {
if (!eyeRef.current) return { x: 0, y: 0 };
if (forceLookX !== undefined && forceLookY !== undefined) {
return { x: forceLookX, y: forceLookY };
}
const eye = eyeRef.current.getBoundingClientRect();
const eyeCenterX = eye.left + eye.width / 2;
const eyeCenterY = eye.top + eye.height / 2;
const deltaX = mouseX - eyeCenterX;
const deltaY = mouseY - eyeCenterY;
const distance = Math.min(Math.sqrt(deltaX ** 2 + deltaY ** 2), maxDistance);
const angle = Math.atan2(deltaY, deltaX);
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
return { x, y };
};
const pupilPosition = calculatePupilPosition();
return (
<div
ref={eyeRef}
className="rounded-full flex items-center justify-center transition-all duration-150"
style={{
width: `${size}px`,
height: isBlinking ? '2px' : `${size}px`,
backgroundColor: eyeColor,
overflow: 'hidden',
}}
>
{!isBlinking && (
<div
className="rounded-full"
style={{
width: `${pupilSize}px`,
height: `${pupilSize}px`,
backgroundColor: pupilColor,
transform: `translate(${pupilPosition.x}px, ${pupilPosition.y}px)`,
transition: 'transform 0.1s ease-out',
}}
/>
)}
</div>
);
};
interface LoginPageProps {
onLogin: (token: string) => void;
}
function LoginPage({ onLogin }: LoginPageProps) {
const [showPassword, setShowPassword] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [mouseX, setMouseX] = useState<number>(0);
const [mouseY, setMouseY] = useState<number>(0);
const [isPurpleBlinking, setIsPurpleBlinking] = useState(false);
const [isBlackBlinking, setIsBlackBlinking] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [isLookingAtEachOther, setIsLookingAtEachOther] = useState(false);
const [isPurplePeeking, setIsPurplePeeking] = useState(false);
const purpleRef = useRef<HTMLDivElement>(null);
const blackRef = useRef<HTMLDivElement>(null);
const yellowRef = useRef<HTMLDivElement>(null);
const orangeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMouseX(e.clientX);
setMouseY(e.clientY);
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
useEffect(() => {
const getRandomBlinkInterval = () => Math.random() * 4000 + 3000;
const scheduleBlink = () => {
const blinkTimeout = setTimeout(() => {
setIsPurpleBlinking(true);
setTimeout(() => {
setIsPurpleBlinking(false);
scheduleBlink();
}, 150);
}, getRandomBlinkInterval());
return blinkTimeout;
};
const timeout = scheduleBlink();
return () => clearTimeout(timeout);
}, []);
useEffect(() => {
const getRandomBlinkInterval = () => Math.random() * 4000 + 3000;
const scheduleBlink = () => {
const blinkTimeout = setTimeout(() => {
setIsBlackBlinking(true);
setTimeout(() => {
setIsBlackBlinking(false);
scheduleBlink();
}, 150);
}, getRandomBlinkInterval());
return blinkTimeout;
};
const timeout = scheduleBlink();
return () => clearTimeout(timeout);
}, []);
useEffect(() => {
if (isTyping) {
setIsLookingAtEachOther(true);
const timer = setTimeout(() => {
setIsLookingAtEachOther(false);
}, 800);
return () => clearTimeout(timer);
} else {
setIsLookingAtEachOther(false);
}
}, [isTyping]);
useEffect(() => {
if (password.length > 0 && showPassword) {
const schedulePeek = () => {
const peekInterval = setTimeout(() => {
setIsPurplePeeking(true);
setTimeout(() => {
setIsPurplePeeking(false);
}, 800);
}, Math.random() * 3000 + 2000);
return peekInterval;
};
const firstPeek = schedulePeek();
return () => clearTimeout(firstPeek);
} else {
setIsPurplePeeking(false);
}
}, [password, showPassword, isPurplePeeking]);
const calculatePosition = (ref: React.RefObject<HTMLDivElement | null>) => {
if (!ref.current) return { faceX: 0, faceY: 0, bodySkew: 0 };
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 3;
const deltaX = mouseX - centerX;
const deltaY = mouseY - centerY;
const faceX = Math.max(-15, Math.min(15, deltaX / 20));
const faceY = Math.max(-10, Math.min(10, deltaY / 30));
const bodySkew = Math.max(-6, Math.min(6, -deltaX / 120));
return { faceX, faceY, bodySkew };
};
const purplePos = calculatePosition(purpleRef);
const blackPos = calculatePosition(blackRef);
const yellowPos = calculatePosition(yellowRef);
const orangePos = calculatePosition(orangeRef);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || "Invalid email or password.");
} else {
onLogin(data.token);
}
} catch {
setError("Network error. Please try again.");
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen grid lg:grid-cols-2">
{/* Left Content Section */}
<div className="relative hidden lg:flex flex-col justify-between bg-gradient-to-br from-primary/90 via-primary to-primary/80 p-12 text-primary-foreground">
<div className="relative z-20">
<div className="flex items-center gap-2 text-lg font-semibold">
<div className="size-8 rounded-lg bg-primary-foreground/10 backdrop-blur-sm flex items-center justify-center">
<Sparkles className="size-4" />
</div>
<span>Eventify</span>
</div>
</div>
<div className="relative z-20 flex items-end justify-center h-[500px]">
{/* Cartoon Characters */}
<div className="relative" style={{ width: '550px', height: '400px' }}>
{/* Purple tall rectangle character */}
<div
ref={purpleRef}
className="absolute bottom-0 transition-all duration-700 ease-in-out"
style={{
left: '70px',
width: '180px',
height: (isTyping || (password.length > 0 && !showPassword)) ? '440px' : '400px',
backgroundColor: '#6C3FF5',
borderRadius: '10px 10px 0 0',
zIndex: 1,
transform: (password.length > 0 && showPassword)
? `skewX(0deg)`
: (isTyping || (password.length > 0 && !showPassword))
? `skewX(${(purplePos.bodySkew || 0) - 12}deg) translateX(40px)`
: `skewX(${purplePos.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}
>
<div
className="absolute flex gap-8 transition-all duration-700 ease-in-out"
style={{
left: (password.length > 0 && showPassword) ? `${20}px` : isLookingAtEachOther ? `${55}px` : `${45 + purplePos.faceX}px`,
top: (password.length > 0 && showPassword) ? `${35}px` : isLookingAtEachOther ? `${65}px` : `${40 + purplePos.faceY}px`,
}}
>
<EyeBall
size={18} pupilSize={7} maxDistance={5} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isPurpleBlinking}
forceLookX={(password.length > 0 && showPassword) ? (isPurplePeeking ? 4 : -4) : isLookingAtEachOther ? 3 : undefined}
forceLookY={(password.length > 0 && showPassword) ? (isPurplePeeking ? 5 : -4) : isLookingAtEachOther ? 4 : undefined}
/>
<EyeBall
size={18} pupilSize={7} maxDistance={5} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isPurpleBlinking}
forceLookX={(password.length > 0 && showPassword) ? (isPurplePeeking ? 4 : -4) : isLookingAtEachOther ? 3 : undefined}
forceLookY={(password.length > 0 && showPassword) ? (isPurplePeeking ? 5 : -4) : isLookingAtEachOther ? 4 : undefined}
/>
</div>
</div>
{/* Black tall rectangle character */}
<div
ref={blackRef}
className="absolute bottom-0 transition-all duration-700 ease-in-out"
style={{
left: '240px',
width: '120px',
height: '310px',
backgroundColor: '#2D2D2D',
borderRadius: '8px 8px 0 0',
zIndex: 2,
transform: (password.length > 0 && showPassword)
? `skewX(0deg)`
: isLookingAtEachOther
? `skewX(${(blackPos.bodySkew || 0) * 1.5 + 10}deg) translateX(20px)`
: (isTyping || (password.length > 0 && !showPassword))
? `skewX(${(blackPos.bodySkew || 0) * 1.5}deg)`
: `skewX(${blackPos.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}
>
<div
className="absolute flex gap-6 transition-all duration-700 ease-in-out"
style={{
left: (password.length > 0 && showPassword) ? `${10}px` : isLookingAtEachOther ? `${32}px` : `${26 + blackPos.faceX}px`,
top: (password.length > 0 && showPassword) ? `${28}px` : isLookingAtEachOther ? `${12}px` : `${32 + blackPos.faceY}px`,
}}
>
<EyeBall
size={16} pupilSize={6} maxDistance={4} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isBlackBlinking}
forceLookX={(password.length > 0 && showPassword) ? -4 : isLookingAtEachOther ? 0 : undefined}
forceLookY={(password.length > 0 && showPassword) ? -4 : isLookingAtEachOther ? -4 : undefined}
/>
<EyeBall
size={16} pupilSize={6} maxDistance={4} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isBlackBlinking}
forceLookX={(password.length > 0 && showPassword) ? -4 : isLookingAtEachOther ? 0 : undefined}
forceLookY={(password.length > 0 && showPassword) ? -4 : isLookingAtEachOther ? -4 : undefined}
/>
</div>
</div>
{/* Orange semi-circle character */}
<div
ref={orangeRef}
className="absolute bottom-0 transition-all duration-700 ease-in-out"
style={{
left: '0px',
width: '240px',
height: '200px',
zIndex: 3,
backgroundColor: '#FF9B6B',
borderRadius: '120px 120px 0 0',
transform: (password.length > 0 && showPassword) ? `skewX(0deg)` : `skewX(${orangePos.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}
>
<div
className="absolute flex gap-8 transition-all duration-200 ease-out"
style={{
left: (password.length > 0 && showPassword) ? `${50}px` : `${82 + (orangePos.faceX || 0)}px`,
top: (password.length > 0 && showPassword) ? `${85}px` : `${90 + (orangePos.faceY || 0)}px`,
}}
>
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D" forceLookX={(password.length > 0 && showPassword) ? -5 : undefined} forceLookY={(password.length > 0 && showPassword) ? -4 : undefined} />
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D" forceLookX={(password.length > 0 && showPassword) ? -5 : undefined} forceLookY={(password.length > 0 && showPassword) ? -4 : undefined} />
</div>
</div>
{/* Yellow tall rectangle character */}
<div
ref={yellowRef}
className="absolute bottom-0 transition-all duration-700 ease-in-out"
style={{
left: '310px',
width: '140px',
height: '230px',
backgroundColor: '#E8D754',
borderRadius: '70px 70px 0 0',
zIndex: 4,
transform: (password.length > 0 && showPassword) ? `skewX(0deg)` : `skewX(${yellowPos.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}
>
<div
className="absolute flex gap-6 transition-all duration-200 ease-out"
style={{
left: (password.length > 0 && showPassword) ? `${20}px` : `${52 + (yellowPos.faceX || 0)}px`,
top: (password.length > 0 && showPassword) ? `${35}px` : `${40 + (yellowPos.faceY || 0)}px`,
}}
>
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D" forceLookX={(password.length > 0 && showPassword) ? -5 : undefined} forceLookY={(password.length > 0 && showPassword) ? -4 : undefined} />
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D" forceLookX={(password.length > 0 && showPassword) ? -5 : undefined} forceLookY={(password.length > 0 && showPassword) ? -4 : undefined} />
</div>
<div
className="absolute w-20 h-[4px] bg-[#2D2D2D] rounded-full transition-all duration-200 ease-out"
style={{
left: (password.length > 0 && showPassword) ? `${10}px` : `${40 + (yellowPos.faceX || 0)}px`,
top: (password.length > 0 && showPassword) ? `${88}px` : `${88 + (yellowPos.faceY || 0)}px`,
}}
/>
</div>
</div>
</div>
<div className="relative z-20 flex items-center gap-8 text-sm text-primary-foreground/60">
<a href="#" className="hover:text-primary-foreground transition-colors">Privacy Policy</a>
<a href="#" className="hover:text-primary-foreground transition-colors">Terms of Service</a>
<a href="#" className="hover:text-primary-foreground transition-colors">Contact</a>
</div>
<div className="absolute inset-0 bg-grid-white/[0.05] bg-[size:20px_20px]" />
<div className="absolute top-1/4 right-1/4 size-64 bg-primary-foreground/10 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 left-1/4 size-96 bg-primary-foreground/5 rounded-full blur-3xl" />
</div>
{/* Right Login Section */}
<div className="flex items-center justify-center p-8 bg-background">
<div className="w-full max-w-[420px]">
<div className="lg:hidden flex items-center justify-center gap-2 text-lg font-semibold mb-12">
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center">
<Sparkles className="size-4 text-primary" />
</div>
<span>Eventify</span>
</div>
<div className="text-center mb-10">
<h1 className="text-3xl font-bold tracking-tight mb-2">Server Monitor</h1>
<p className="text-muted-foreground text-sm">Sign in to access the dashboard</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium">Email</Label>
<Input
id="email"
type="email"
placeholder="admin@eventifyplus.com"
value={email}
autoComplete="off"
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setIsTyping(true)}
onBlur={() => setIsTyping(false)}
required
className="h-12 bg-background border-border/60 focus:border-primary"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium">Password</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="h-12 pr-10 bg-background border-border/60 focus:border-primary"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
{showPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox id="remember" />
<Label htmlFor="remember" className="text-sm font-normal cursor-pointer">
Remember for 30 days
</Label>
</div>
</div>
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg">
{error}
</div>
)}
<Button
type="submit"
className="w-full h-12 text-base font-medium cursor-pointer"
size="lg"
disabled={isLoading}
>
{isLoading ? "Signing in..." : "Log in"}
</Button>
</form>
<div className="text-center text-sm text-muted-foreground mt-8">
Eventify Server Health Monitor
</div>
</div>
</div>
</div>
);
}
export { LoginPage };

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -23,7 +23,10 @@ export function useServerHealth() {
const query = useQuery<ServerOverview>({
queryKey: ["server-overview"],
queryFn: async () => {
const res = await fetch("/api/overview")
const token = localStorage.getItem("eventify-auth-token")
const res = await fetch("/api/overview", {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
if (!res.ok) throw new Error(`Server responded with ${res.status}`)
return res.json()
},