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:
jeffusion
2026-03-03 16:32:59 +08:00
parent 0b5cbfd5ba
commit ee1d8f70f7
6 changed files with 192 additions and 90 deletions

View File

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

View File

@@ -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>

View File

@@ -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 (

View File

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

View File

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

View File

@@ -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,25 +29,44 @@ 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 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="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>
<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="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="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">&gt;</span> enter_admin_password
</label>
</div>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="password"></Label>
<div className="relative group">
<Input
id="password"
type="password"
@@ -56,11 +74,42 @@ export function LoginPage() {
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>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button onClick={handleLogin} disabled={isLoading} className="w-full">
{isLoading ? '验证中...' : '登录'}
</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>