feat(ui): premium home page with gradient glass design

- Gradient mesh background with brand colors
- Glass cards with backdrop-blur effects
- Three Pillars section matching README branding
- Command palette search bar with ⌘K hint
- Quick launch icons (G/C/A + accounts + doctor)
- Click-to-copy commands functionality
- Animated health status badge with pulse
- Micro-animations on hover (lift, scale, opacity)
- Responsive bento-style layout
This commit is contained in:
kaitranntt committed 2025-12-13 02:15:57 -05:00
1 parent a2d049c604
commit dbc13718ef
1 file changed
+312 -188
+312 -188
View File
@@ -1,258 +1,382 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { StatCard } from '@/components/stat-card';
import { HeroSection } from '@/components/hero-section';
import { QuickCommands } from '@/components/quick-commands';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { import {
Key, Key,
Zap, Zap,
Users, Users,
Activity, Search,
Plus,
Stethoscope, Stethoscope,
BookOpen,
FolderOpen,
AlertTriangle, AlertTriangle,
Terminal,
ArrowRight,
Command,
Sparkles,
} from 'lucide-react'; } from 'lucide-react';
import { useOverview } from '@/hooks/use-overview'; import { useOverview } from '@/hooks/use-overview';
import { useSharedSummary } from '@/hooks/use-shared'; import { useSharedSummary } from '@/hooks/use-shared';
import { cn } from '@/lib/utils';
const HEALTH_VARIANTS = { const HEALTH_STATUS = {
ok: 'success', ok: { label: 'Operational', class: 'bg-emerald-500/20 text-emerald-500 border-emerald-500/30' },
warning: 'warning', warning: { label: 'Degraded', class: 'bg-amber-500/20 text-amber-500 border-amber-500/30' },
error: 'error', error: { label: 'Issues', class: 'bg-red-500/20 text-red-500 border-red-500/30' },
} as const; } as const;
// Three Pillars configuration - matches README branding
const THREE_PILLARS = [
{
id: 'api',
title: 'API Keys',
description: 'Configure GLM, Kimi with your own API keys',
icon: Key,
path: '/api',
gradient: 'from-orange-500/20 to-amber-500/10',
iconBg: 'bg-orange-500/20 text-orange-500',
},
{
id: 'oauth',
title: 'OAuth Providers',
description: 'Gemini, Codex, Antigravity - zero config',
icon: Zap,
path: '/cliproxy',
gradient: 'from-blue-500/20 to-cyan-500/10',
iconBg: 'bg-blue-500/20 text-blue-500',
},
{
id: 'accounts',
title: 'Multiple Accounts',
description: 'Isolated Claude instances for work & personal',
icon: Users,
path: '/accounts',
gradient: 'from-violet-500/20 to-purple-500/10',
iconBg: 'bg-violet-500/20 text-violet-500',
},
];
// Quick launch shortcuts
const QUICK_LAUNCH = [
{ key: 'G', label: 'Gemini', command: 'ccs gemini' },
{ key: 'C', label: 'Codex', command: 'ccs codex' },
{ key: 'A', label: 'Agy', command: 'ccs agy' },
];
export function HomePage() { export function HomePage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: overview, isLoading: isOverviewLoading } = useOverview(); const { data: overview, isLoading: isOverviewLoading } = useOverview();
const { data: shared, isLoading: isSharedLoading } = useSharedSummary(); const { data: shared, isLoading: isSharedLoading } = useSharedSummary();
if (isOverviewLoading || isSharedLoading) { if (isOverviewLoading || isSharedLoading) {
return ( return <HomePageSkeleton />;
<div className="p-6 space-y-6">
{/* Hero Skeleton */}
<div className="rounded-xl border p-6">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-lg" />
<div>
<Skeleton className="h-7 w-[180px] mb-2" />
<Skeleton className="h-4 w-[220px]" />
</div>
</div>
</div>
{/* Stats Skeleton */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="space-y-3 p-4 border rounded-xl">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-lg" />
<div className="flex-1">
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="h-7 w-12" />
</div>
</div>
</div>
))}
</div>
{/* Quick Actions Skeleton */}
<div className="border rounded-xl p-6 space-y-4">
<Skeleton className="h-6 w-[140px]" />
<div className="flex flex-wrap gap-3">
<Skeleton className="h-10 w-[140px] rounded-md" />
<Skeleton className="h-10 w-[120px] rounded-md" />
<Skeleton className="h-10 w-[150px] rounded-md" />
</div>
</div>
{/* Quick Commands Skeleton */}
<div className="border rounded-xl p-6 space-y-4">
<Skeleton className="h-6 w-[160px]" />
<div className="grid grid-cols-2 gap-2">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-14 rounded-lg" />
))}
</div>
</div>
</div>
);
} }
const healthVariant = overview?.health const healthInfo = overview?.health?.status
? HEALTH_VARIANTS[overview.health.status as keyof typeof HEALTH_VARIANTS] ? HEALTH_STATUS[overview.health.status as keyof typeof HEALTH_STATUS]
: undefined; : HEALTH_STATUS.ok;
const pillarCounts = {
api: overview?.profiles ?? 0,
oauth: overview?.cliproxy ?? 0,
accounts: overview?.accounts ?? 0,
};
return ( return (
<div className="p-6 max-w-7xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500"> <div className="relative min-h-screen">
{/* Hero Section */} {/* Gradient Mesh Background */}
<HeroSection <div className="fixed inset-0 -z-10 overflow-hidden">
version={overview?.version} <div className="absolute -top-1/2 -right-1/2 w-full h-full bg-gradient-to-bl from-primary/5 via-transparent to-transparent rounded-full blur-3xl" />
healthStatus={overview?.health?.status} <div className="absolute -bottom-1/2 -left-1/2 w-full h-full bg-gradient-to-tr from-primary/5 via-transparent to-transparent rounded-full blur-3xl" />
healthPassed={overview?.health?.passed} </div>
healthTotal={overview?.health?.total}
/> <div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Hero Header */}
<header className="relative">
<Card className="overflow-hidden border-primary/10 bg-card/80 backdrop-blur-xl">
<CardContent className="p-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/20">
<Sparkles className="w-6 h-6 text-primary" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">CCS</h1>
<p className="text-sm text-muted-foreground">Multi-Account AI Management</p>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline" className="font-mono text-xs px-3 py-1">
v{overview?.version ?? '0.0.0'}
</Badge>
<Badge variant="outline" className={cn('px-3 py-1 border', healthInfo.class)}>
<span className="w-1.5 h-1.5 rounded-full bg-current mr-2 animate-pulse" />
{healthInfo.label}
</Badge>
</div>
</div>
{/* Command Palette Search Bar */}
<div className="mt-6 group cursor-pointer" onClick={() => navigate('/health')}>
<div className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border/50 bg-muted/30 hover:bg-muted/50 hover:border-primary/30 transition-all">
<Search className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground flex-1">
Search commands, profiles, or run diagnostics...
</span>
<kbd className="hidden sm:inline-flex h-6 items-center gap-1 rounded border bg-muted px-2 font-mono text-xs text-muted-foreground">
<Command className="w-3 h-3" />K
</kbd>
</div>
</div>
</CardContent>
</Card>
</header>
{/* Configuration Warning */} {/* Configuration Warning */}
{shared?.symlinkStatus && !shared.symlinkStatus.valid && ( {shared?.symlinkStatus && !shared.symlinkStatus.valid && (
<Alert variant="warning" className="animate-in zoom-in-95 duration-300"> <Alert
variant="warning"
className="animate-in zoom-in-95 duration-300 border-amber-500/30 bg-amber-500/5"
>
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>Configuration Required</AlertTitle> <AlertTitle>Configuration Required</AlertTitle>
<AlertDescription>{shared.symlinkStatus.message}</AlertDescription> <AlertDescription>{shared.symlinkStatus.message}</AlertDescription>
</Alert> </Alert>
)} )}
{/* Stats Grid */} {/* Three Pillars Section */}
<section> <section className="space-y-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center gap-2">
<h2 className="text-xl font-semibold tracking-tight">System Status</h2> <h2 className="text-lg font-semibold tracking-tight">The Three Pillars</h2>
<span className="text-xs text-muted-foreground">— What do you want to do?</span>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatCard {THREE_PILLARS.map((pillar) => {
title="API Profiles" const Icon = pillar.icon;
value={overview?.profiles ?? 0} const count = pillarCounts[pillar.id as keyof typeof pillarCounts];
icon={Key} return (
variant="accent" <Card
subtitle="Settings-based" key={pillar.id}
onClick={() => navigate('/api')} className={cn(
/> 'group cursor-pointer overflow-hidden transition-all duration-300',
<StatCard 'hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-1',
title="CLIProxy" 'border-border/50 bg-gradient-to-br',
value={overview?.cliproxy ?? 0} pillar.gradient
icon={Zap} )}
variant="accent" onClick={() => navigate(pillar.path)}
subtitle={`${overview?.cliproxyProviders ?? 0} auth + ${overview?.cliproxyVariants ?? 0} custom`} >
onClick={() => navigate('/cliproxy')} <CardContent className="p-6 space-y-4">
/> <div className="flex items-start justify-between">
<StatCard <div
title="Accounts" className={cn(
value={overview?.accounts ?? 0} 'p-3 rounded-xl transition-transform group-hover:scale-110',
icon={Users} pillar.iconBg
variant="default" )}
subtitle="Isolated instances" >
onClick={() => navigate('/accounts')} <Icon className="w-6 h-6" />
/> </div>
<StatCard <span className="text-3xl font-bold font-mono opacity-80">{count}</span>
title="Health" </div>
value={overview?.health ? `${overview.health.passed}/${overview.health.total}` : '-'} <div className="space-y-1">
icon={Activity} <h3 className="font-semibold text-lg group-hover:text-primary transition-colors">
variant={healthVariant} {pillar.title}
subtitle="System checks" </h3>
onClick={() => navigate('/health')} <p className="text-sm text-muted-foreground leading-relaxed">
/> {pillar.description}
</p>
</div>
<div className="flex items-center text-sm text-primary opacity-0 group-hover:opacity-100 transition-opacity">
<span>Configure</span>
<ArrowRight className="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" />
</div>
</CardContent>
</Card>
);
})}
</div> </div>
</section> </section>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> {/* Quick Launch & Health Grid */}
{/* Left Column: Quick Actions & Shared Data */} <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="space-y-8 lg:col-span-2"> {/* Quick Launch */}
{/* Quick Actions */} <Card className="lg:col-span-2 border-border/50 bg-card/80 backdrop-blur-sm">
<section> <CardContent className="p-6 space-y-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold tracking-tight">Quick Actions</h2> <h3 className="font-semibold flex items-center gap-2">
<Terminal className="w-4 h-4 text-muted-foreground" />
Quick Launch
</h3>
<span className="text-xs text-muted-foreground">Click to copy command</span>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
<Card {QUICK_LAUNCH.map((item) => (
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]" <button
onClick={() => navigate('/api')} key={item.key}
className="group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/20 hover:bg-primary/10 hover:border-primary/30 transition-all"
onClick={() => navigator.clipboard.writeText(item.command)}
title={item.command}
> >
<CardContent className="p-6 flex items-center gap-4"> <kbd className="w-10 h-10 flex items-center justify-center rounded-lg bg-muted font-mono text-lg font-bold group-hover:bg-primary/20 group-hover:text-primary transition-colors">
<div className="p-3 rounded-xl bg-primary/10 text-primary group-hover:scale-110 transition-transform"> {item.key}
<Plus className="w-6 h-6" /> </kbd>
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors">
{item.label}
</span>
</button>
))}
{/* Account shortcuts */}
{overview?.accounts && overview.accounts > 0 && (
<>
<button
className="group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/20 hover:bg-violet-500/10 hover:border-violet-500/30 transition-all"
onClick={() => navigate('/accounts')}
title="Switch account"
>
<div className="w-10 h-10 flex items-center justify-center rounded-lg bg-muted font-mono text-lg font-bold group-hover:bg-violet-500/20 group-hover:text-violet-500 transition-colors">
<Users className="w-5 h-5" />
</div> </div>
<div> <span className="text-xs text-muted-foreground group-hover:text-violet-500 transition-colors">
<h3 className="font-semibold text-lg">New Profile</h3> Accounts
<p className="text-muted-foreground text-sm">Create a new API profile</p> </span>
</button>
</>
)}
{/* Doctor shortcut */}
<button
className="group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/20 hover:bg-emerald-500/10 hover:border-emerald-500/30 transition-all"
onClick={() => navigate('/health')}
title="Run diagnostics"
>
<div className="w-10 h-10 flex items-center justify-center rounded-lg bg-muted font-mono text-lg font-bold group-hover:bg-emerald-500/20 group-hover:text-emerald-500 transition-colors">
<Stethoscope className="w-5 h-5" />
</div>
<span className="text-xs text-muted-foreground group-hover:text-emerald-500 transition-colors">
Doctor
</span>
</button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card {/* Health & Resources */}
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]" <Card className="border-border/50 bg-card/80 backdrop-blur-sm">
<CardContent className="p-6 space-y-6">
{/* Health Summary */}
<div className="space-y-3">
<h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wider">
System Health
</h3>
<div
className="flex items-center justify-between p-4 rounded-xl bg-muted/30 hover:bg-muted/50 cursor-pointer transition-colors"
onClick={() => navigate('/health')} onClick={() => navigate('/health')}
> >
<CardContent className="p-6 flex items-center gap-4"> <div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-primary/10 text-primary group-hover:scale-110 transition-transform"> <div
<Stethoscope className="w-6 h-6" /> className={cn(
'w-3 h-3 rounded-full',
overview?.health?.status === 'ok'
? 'bg-emerald-500'
: overview?.health?.status === 'warning'
? 'bg-amber-500'
: 'bg-red-500'
)}
/>
<span className="font-mono text-2xl font-bold">
{overview?.health?.passed ?? 0}/{overview?.health?.total ?? 0}
</span>
</div>
<span className="text-xs text-muted-foreground">checks passed</span>
</div> </div>
<div>
<h3 className="font-semibold text-lg">Run Doctor</h3>
<p className="text-muted-foreground text-sm">Check system health</p>
</div> </div>
</CardContent>
</Card>
<Card {/* Resources Summary */}
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]" <div className="space-y-3">
onClick={() => window.open('https://github.com/kaitranntt/ccs', '_blank')} <h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wider">
> Shared Resources
<CardContent className="p-6 flex items-center gap-4"> </h3>
<div className="p-3 rounded-xl bg-muted text-muted-foreground group-hover:text-primary transition-colors"> <div
<BookOpen className="w-6 h-6" /> className="grid grid-cols-3 gap-2 cursor-pointer"
</div>
<div>
<h3 className="font-semibold text-lg">Documentation</h3>
<p className="text-muted-foreground text-sm">View guides & reference</p>
</div>
</CardContent>
</Card>
<Card
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]"
onClick={() => navigate('/shared')} onClick={() => navigate('/shared')}
> >
<CardContent className="p-6 flex items-center gap-4"> <div className="flex flex-col items-center p-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
<div className="p-3 rounded-xl bg-muted text-muted-foreground group-hover:text-primary transition-colors"> <span className="font-mono text-xl font-bold text-primary">
<FolderOpen className="w-6 h-6" />
</div>
<div>
<h3 className="font-semibold text-lg">Shared Data</h3>
<p className="text-muted-foreground text-sm">Manage resources</p>
</div>
</CardContent>
</Card>
</div>
</section>
{/* Shared Data Stats */}
<section>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold tracking-tight">Resource Summary</h2>
</div>
<Card>
<CardContent className="p-6">
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-center justify-center p-4 rounded-xl bg-muted/30 hover:bg-muted/50 transition-colors">
<span className="text-3xl font-bold font-mono text-primary mb-1">
{shared?.commands ?? 0} {shared?.commands ?? 0}
</span> </span>
<span className="text-sm font-medium text-muted-foreground">Commands</span> <span className="text-xs text-muted-foreground">cmds</span>
</div> </div>
<div className="flex flex-col items-center justify-center p-4 rounded-xl bg-muted/30 hover:bg-muted/50 transition-colors"> <div className="flex flex-col items-center p-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
<span className="text-3xl font-bold font-mono text-primary mb-1"> <span className="font-mono text-xl font-bold text-primary">
{shared?.skills ?? 0} {shared?.skills ?? 0}
</span> </span>
<span className="text-sm font-medium text-muted-foreground">Skills</span> <span className="text-xs text-muted-foreground">skills</span>
</div> </div>
<div className="flex flex-col items-center justify-center p-4 rounded-xl bg-muted/30 hover:bg-muted/50 transition-colors"> <div className="flex flex-col items-center p-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
<span className="text-3xl font-bold font-mono text-primary mb-1"> <span className="font-mono text-xl font-bold text-primary">
{shared?.agents ?? 0} {shared?.agents ?? 0}
</span> </span>
<span className="text-sm font-medium text-muted-foreground">Agents</span> <span className="text-xs text-muted-foreground">agents</span>
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</section>
</div> </div>
</div>
</div>
);
}
{/* Right Column: Quick Commands */} // Skeleton loader component
<div className="space-y-4"> function HomePageSkeleton() {
<h2 className="text-xl font-semibold tracking-tight">Command Palette</h2> return (
<QuickCommands /> <div className="p-6 max-w-6xl mx-auto space-y-8">
{/* Hero Skeleton */}
<div className="rounded-xl border p-8">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-xl" />
<div>
<Skeleton className="h-7 w-[120px] mb-2" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
<Skeleton className="h-12 w-full mt-6 rounded-xl" />
</div>
{/* Pillars Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="p-6 border rounded-xl space-y-4">
<div className="flex items-start justify-between">
<Skeleton className="h-12 w-12 rounded-xl" />
<Skeleton className="h-8 w-8" />
</div>
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-full" />
</div>
))}
</div>
{/* Quick Launch Skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 border rounded-xl p-6">
<Skeleton className="h-6 w-32 mb-4" />
<div className="grid grid-cols-6 gap-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-20 rounded-xl" />
))}
</div>
</div>
<div className="border rounded-xl p-6 space-y-4">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-16 rounded-xl" />
<Skeleton className="h-4 w-24" />
<div className="grid grid-cols-3 gap-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-16 rounded-lg" />
))}
</div>
</div> </div>
</div> </div>
</div> </div>