mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
style(frontend): redesign UI with dark tech aesthetic
Complete visual overhaul with cyberpunk-inspired dark theme: - New color palette: teal primary (#14b8a6), zinc grays - Glass panel effects with backdrop blur - Grid pattern backgrounds - Glow effects on interactive elements Component updates: - LoginPage: Terminal-style login with animated effects - DashboardPage: Collapsible sidebar with active indicators - DataTable: Glass panel styling with hover states - RepositoryManager: Tech-styled search and pagination - WebhookToggleButton: Glow effect on enable action CSS additions: - Inter + JetBrains Mono fonts - Custom utilities: glass-panel, tech-glow, bg-grid-pattern - Updated CSS variables for dark mode
This commit is contained in:
@@ -32,14 +32,14 @@ export function DataTable<TData, TValue>({
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<div className="rounded-xl border border-border/50 overflow-hidden glass-panel">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHeader className="bg-zinc-900 text-zinc-400 uppercase tracking-wider font-mono text-xs">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
<TableHead key={header.id} className="font-mono text-zinc-400">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
@@ -58,6 +58,7 @@ export function DataTable<TData, TValue>({
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="hover:bg-zinc-900/50 transition-colors border-border/50"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
@@ -68,8 +69,13 @@ export function DataTable<TData, TValue>({
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
<TableCell colSpan={columns.length} className="h-48 text-center text-zinc-500 font-mono">
|
||||
<div className="flex flex-col items-center justify-center space-y-3">
|
||||
<div className="p-3 rounded-full bg-zinc-900 border border-white/5 text-zinc-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-database"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>
|
||||
</div>
|
||||
<p>未找到匹配的仓库</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@@ -13,21 +13,21 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
|
||||
function DataTableSkeleton() {
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<div className="rounded-xl border border-border/50 overflow-hidden glass-panel">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40%]"><Skeleton className="h-5 w-24" /></TableHead>
|
||||
<TableHead className="w-[30%]"><Skeleton className="h-5 w-24" /></TableHead>
|
||||
<TableHead className="w-[30%] text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableHead>
|
||||
<TableHeader className="bg-zinc-900 border-b border-border/50">
|
||||
<TableRow className="border-border/50">
|
||||
<TableHead className="w-[40%]"><Skeleton className="h-5 w-24 bg-zinc-800" /></TableHead>
|
||||
<TableHead className="w-[30%]"><Skeleton className="h-5 w-24 bg-zinc-800" /></TableHead>
|
||||
<TableHead className="w-[30%] text-right"><Skeleton className="h-5 w-16 ml-auto bg-zinc-800" /></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell><Skeleton className="h-5 w-3/4" /></TableCell>
|
||||
<TableCell><Skeleton className="h-6 w-20" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-8 w-16 ml-auto" /></TableCell>
|
||||
<TableRow key={i} className="border-border/50">
|
||||
<TableCell><Skeleton className="h-5 w-3/4 bg-zinc-800/80" /></TableCell>
|
||||
<TableCell><Skeleton className="h-6 w-20 bg-zinc-800/80 rounded-full" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-8 w-16 ml-auto bg-zinc-800/80" /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -61,16 +61,15 @@ export function RepositoryManager() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="font-semibold text-lg md:text-2xl">仓库 Webhook 管理</h1>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex justify-end mb-6">
|
||||
<div className="relative w-full md:w-auto">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="搜索仓库..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px]"
|
||||
className="pl-9 w-full sm:w-[300px] md:w-[250px] lg:w-[300px] bg-zinc-900/50 border-border/50 text-zinc-200 placeholder:text-zinc-600 focus-visible:ring-1 focus-visible:ring-primary/50 focus-visible:border-primary transition-all font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,20 +77,27 @@ export function RepositoryManager() {
|
||||
{isLoading ? (
|
||||
<DataTableSkeleton />
|
||||
) : isError ? (
|
||||
<div className="p-4">
|
||||
{/* The original Alert component was removed, so this will now just show the error message */}
|
||||
<p className="text-red-500">加载仓库列表失败: {error.message}</p>
|
||||
<div className="p-6 rounded-xl border border-rose-500/20 bg-rose-500/5 glass-panel">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 bg-rose-500/10 rounded-lg text-rose-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-mono text-sm font-medium text-rose-500">System Error_</h3>
|
||||
<p className="font-mono text-xs text-rose-400/80">加载仓库列表失败: {error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DataTable columns={columns} data={repos} />
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between w-full mt-4 space-x-4">
|
||||
<div className="text-sm text-muted-foreground flex-shrink-0">
|
||||
第 {page} 页 / 共 {totalPages} 页 (共 {totalCount} 个仓库)
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between w-full mt-6 gap-4">
|
||||
<div className="font-mono text-xs text-zinc-400 flex-shrink-0 bg-zinc-900/50 px-3 py-1.5 rounded-md border border-white/5">
|
||||
第 <span className="text-zinc-200">{page}</span> 页 / 共 <span className="text-zinc-200">{totalPages}</span> 页 <span className="text-zinc-600 mx-1">|</span> 共 <span className="text-zinc-200">{totalCount}</span> 个仓库
|
||||
</div>
|
||||
<Pagination className="flex-shrink-0 justify-end w-auto">
|
||||
<PaginationContent>
|
||||
<Pagination className="flex-shrink-0 w-auto mx-0">
|
||||
<PaginationContent className="gap-2">
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
@@ -99,7 +105,7 @@ export function RepositoryManager() {
|
||||
e.preventDefault();
|
||||
setPage(p => Math.max(1, p - 1));
|
||||
}}
|
||||
className={page <= 1 ? "pointer-events-none opacity-50" : ""}
|
||||
className={`border border-border/50 hover:bg-zinc-800 hover:text-primary hover:border-primary/50 font-mono text-xs transition-colors ${page <= 1 ? "pointer-events-none opacity-30 bg-zinc-900/30" : "bg-zinc-900 text-zinc-400"}`}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
@@ -109,7 +115,7 @@ export function RepositoryManager() {
|
||||
e.preventDefault();
|
||||
setPage(p => Math.min(totalPages, p + 1));
|
||||
}}
|
||||
className={page >= totalPages ? "pointer-events-none opacity-50" : ""}
|
||||
className={`border border-border/50 hover:bg-zinc-800 hover:text-primary hover:border-primary/50 font-mono text-xs transition-colors ${page >= totalPages ? "pointer-events-none opacity-30 bg-zinc-900/30" : "bg-zinc-900 text-zinc-400"}`}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import type { Repository } from "@/services/repositoryService"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { WebhookToggleButton } from "@/components/WebhookToggleButton"
|
||||
|
||||
export const columns: ColumnDef<Repository>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "仓库名称",
|
||||
cell: ({ row }) => <div className="font-medium">{row.getValue("name")}</div>,
|
||||
cell: ({ row }) => <div className="font-medium text-zinc-100 text-sm">{row.getValue("name")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "webhook_status",
|
||||
@@ -18,15 +17,16 @@ export const columns: ColumnDef<Repository>[] = [
|
||||
const status = row.getValue("webhook_status") as Repository["webhook_status"]
|
||||
const isActive = status === 'active'
|
||||
return (
|
||||
<Badge variant={isActive ? "default" : "outline"}>
|
||||
<div className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold border ${isActive ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' : 'bg-transparent text-zinc-500 border-zinc-700'}`}>
|
||||
{isActive && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" style={{ boxShadow: '0 0 8px 1px rgba(52, 211, 153, 0.6)' }}></span>}
|
||||
{isActive ? '已启用' : '未启用'}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <div className="text-right">操作</div>,
|
||||
header: () => <div className="text-right text-zinc-400">操作</div>,
|
||||
cell: ({ row }) => {
|
||||
const repo = row.original
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -32,14 +33,26 @@ export function WebhookToggleButton({ repoName, status, hookId }: WebhookToggleB
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={status === 'active' ? 'destructive' : 'default'}
|
||||
variant={status === 'active' ? 'outline' : 'default'}
|
||||
size="sm"
|
||||
className={
|
||||
status === 'active'
|
||||
? "border-rose-500/50 bg-transparent text-rose-500 hover:bg-rose-500/10 hover:text-rose-400 transition-colors"
|
||||
: "bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-300 hover:shadow-[0_0_15px_rgba(45,212,191,0.5)] tech-glow"
|
||||
}
|
||||
onClick={() => mutation.mutate()}
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
{mutation.isPending
|
||||
? '处理中...'
|
||||
: status === 'active' ? '停用' : '启用'}
|
||||
{mutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||
<span className="font-mono text-xs">处理中...</span>
|
||||
</>
|
||||
) : status === 'active' ? (
|
||||
<span className="font-mono text-xs">停用</span>
|
||||
) : (
|
||||
<span className="font-mono text-xs">启用</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,53 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background: 240 10% 98%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--primary: 180 100% 35%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--accent: 180 100% 35%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--ring: 180 100% 35%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--background: 240 10% 4%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card: 240 10% 6%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover: 240 10% 6%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--primary: 175 90% 45%;
|
||||
--primary-foreground: 240 10% 4%;
|
||||
--secondary: 240 5% 15%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--muted: 240 5% 15%;
|
||||
--muted-foreground: 240 5% 65%;
|
||||
--accent: 175 90% 45%;
|
||||
--accent-foreground: 240 10% 4%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--border: 240 5% 15%;
|
||||
--input: 240 5% 15%;
|
||||
--ring: 175 90% 45%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +56,32 @@
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground font-sans antialiased selection:bg-primary/30 selection:text-primary;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply tracking-tight;
|
||||
}
|
||||
code, pre, .font-mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.font-sans {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.bg-grid-pattern {
|
||||
background-size: 40px 40px;
|
||||
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
||||
}
|
||||
.glass-panel {
|
||||
@apply bg-zinc-950/50 backdrop-blur-xl border border-white/10 shadow-2xl;
|
||||
}
|
||||
.tech-glow {
|
||||
box-shadow: 0 0 20px -5px hsl(var(--primary) / 0.5);
|
||||
}
|
||||
.tech-glow:hover {
|
||||
box-shadow: 0 0 30px -5px hsl(var(--primary) / 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useState } from 'react';
|
||||
import api from '@/lib/api';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Bot } from 'lucide-react';
|
||||
import { Bot, Terminal, ShieldCheck, ArrowRight, Activity } from 'lucide-react';
|
||||
|
||||
export function LoginPage() {
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -30,37 +29,87 @@ export function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full lg:grid lg:min-h-screen lg:grid-cols-2">
|
||||
<div className="hidden bg-gray-900 lg:flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Bot className="h-24 w-24 text-gray-500 mx-auto mb-6" />
|
||||
<h1 className="text-4xl font-bold text-white">Gitea AI Assistant</h1>
|
||||
<p className="mt-4 text-gray-400">智能代码审查,自动化您的工作流</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-12 min-h-screen">
|
||||
<div className="mx-auto grid w-[350px] gap-6">
|
||||
<div className="grid gap-2 text-center">
|
||||
<h1 className="text-3xl font-bold">登录</h1>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
请输入您的管理员密码以继续
|
||||
</p>
|
||||
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-zinc-950">
|
||||
{/* Background grid and gradient effects */}
|
||||
<div className="absolute inset-0 bg-grid-pattern opacity-10"></div>
|
||||
<div className="absolute top-[-20%] left-[-10%] h-[500px] w-[500px] rounded-full bg-primary/20 blur-[120px] pointer-events-none"></div>
|
||||
<div className="absolute bottom-[-20%] right-[-10%] h-[500px] w-[500px] rounded-full bg-primary/10 blur-[100px] pointer-events-none"></div>
|
||||
|
||||
<div className="z-10 w-full max-w-md px-4 sm:px-6 relative">
|
||||
<div className="glass-panel relative rounded-2xl p-8 sm:p-10 transition-all duration-500 hover:border-primary/20">
|
||||
{/* Decorative terminal dots */}
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-rose-500/80 shadow-[0_0_5px_rgba(244,63,94,0.5)]"></div>
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-amber-500/80 shadow-[0_0_5px_rgba(245,158,11,0.5)]"></div>
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500/80 shadow-[0_0_5px_rgba(16,185,129,0.5)]"></div>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && !isLoading && handleLogin()}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="mb-10 mt-6 flex flex-col items-center text-center">
|
||||
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-zinc-900 border border-primary/20 shadow-[0_0_20px_rgba(var(--primary),0.15)] ring-1 ring-primary/10 relative group">
|
||||
<div className="absolute inset-0 rounded-2xl bg-primary/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<Bot className="h-8 w-8 text-primary relative z-10" />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button onClick={handleLogin} disabled={isLoading} className="w-full">
|
||||
{isLoading ? '验证中...' : '登录'}
|
||||
<h1 className="mb-2 text-2xl font-bold tracking-tight text-white sm:text-3xl">Gitea AI Assistant</h1>
|
||||
<div className="flex items-center gap-2 text-xs font-mono text-primary/70 bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
||||
</span>
|
||||
[SYSTEM] authentication_required
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="password" className="text-xs font-mono font-medium text-zinc-400 flex items-center gap-2">
|
||||
<span className="text-primary font-bold">></span> enter_admin_password
|
||||
</label>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && !isLoading && handleLogin()}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
className="h-12 border-zinc-800 bg-zinc-900/50 font-mono text-zinc-100 placeholder:text-zinc-700 focus-visible:border-primary/50 focus-visible:ring-primary/20 transition-all duration-300"
|
||||
/>
|
||||
<Terminal className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-600 transition-colors group-focus-within:text-primary/70" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-rose-500/20 bg-rose-500/10 px-3 py-3 text-sm text-rose-400 animate-in fade-in slide-in-from-top-1">
|
||||
<Activity className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<p className="font-mono text-xs leading-relaxed">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
disabled={isLoading}
|
||||
className="tech-glow group relative mt-4 h-12 w-full overflow-hidden bg-primary text-primary-foreground transition-all hover:bg-primary/90 disabled:opacity-70 disabled:pointer-events-none"
|
||||
>
|
||||
<div className="absolute inset-0 flex h-full w-full justify-center [transform:skew(-12deg)_translateX(-150%)] group-hover:duration-1000 group-hover:[transform:skew(-12deg)_translateX(150%)]">
|
||||
<div className="relative h-full w-12 bg-white/20"></div>
|
||||
</div>
|
||||
<span className="relative flex items-center gap-2 font-mono font-semibold tracking-wide">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground"></div>
|
||||
VERIFYING...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
AUTHORIZE
|
||||
<ArrowRight className="h-4 w-4 opacity-70 transition-transform duration-300 group-hover:translate-x-1 group-hover:opacity-100" />
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user