Education: - CourseCard: lessons count badge, progress bar, backdrop-blur on badges - EducationView: framer-motion stagger on grid - Filters: interactive color-coded pills (Beginner/Intermediate/Advanced) - MyCoursesView: stagger animation, semantic token migration Gamification: - LeaderboardView: gold/silver/bronze podium styling with glow + accents - AchievementCard: shine sweep animation on hover, lift effect - AchievementsView: stagger animation with filter re-animation - XPBar: semantic token fix Developer dashboard: - API key copy-to-clipboard with icon toggle - Status indicator badges with animated pulse dot Commerce/Admin: - WishlistView: stagger animation, hover lift - PurchasesView: stagger on list items - Admin views: consistent headers, semantic tokens (text-white → text-foreground) 18 files modified, all text-white → text-foreground migrations Co-authored-by: Cursor <cursoragent@cursor.com>
300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Card } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { StatCard } from '../dashboard/StatCard';
|
|
import { CreateAPIKeyModal } from './modals/CreateAPIKeyModal';
|
|
import {
|
|
Key,
|
|
Activity,
|
|
Globe,
|
|
Plus,
|
|
Trash2,
|
|
Eye,
|
|
ExternalLink,
|
|
Loader2,
|
|
FileText,
|
|
Copy,
|
|
Check,
|
|
} from 'lucide-react';
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
|
import { developerService } from '../../services/developerService';
|
|
import { logger } from '@/utils/logger';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { SwaggerUIDoc } from './SwaggerUI';
|
|
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
|
|
|
|
interface ApiKey {
|
|
id: string;
|
|
name: string;
|
|
prefix: string;
|
|
created: string;
|
|
lastUsed: string;
|
|
status: 'active' | 'revoked';
|
|
}
|
|
|
|
export const DeveloperDashboardView: React.FC = () => {
|
|
const { addToast } = useToast();
|
|
const { copy } = useCopyToClipboard();
|
|
const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
|
|
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [stats, setStats] = useState<any>({});
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
|
|
const handleCopyKey = async (key: ApiKey) => {
|
|
await copy(key.prefix);
|
|
setCopiedKeyId(key.id);
|
|
addToast('Key prefix copied to clipboard', 'success');
|
|
setTimeout(() => setCopiedKeyId(null), 2000);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [keysData, statsData] = await Promise.all([
|
|
developerService.listKeys(),
|
|
developerService.getStats(),
|
|
]);
|
|
setKeys(keysData);
|
|
setStats(statsData);
|
|
} catch (e) {
|
|
logger.error('Error loading developer dashboard data', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
const handleCreateKey = async (data: { name: string; scopes: string[] }) => {
|
|
try {
|
|
const newKey = await developerService.createKey(data);
|
|
setKeys([newKey, ...keys]);
|
|
addToast('API Key created successfully', 'success');
|
|
} catch (e) {
|
|
addToast('Failed to create API key', 'error');
|
|
}
|
|
};
|
|
|
|
const handleRevoke = async (id: string) => {
|
|
if (confirm('Are you sure you want to revoke this key?')) {
|
|
await developerService.revokeKey(id);
|
|
// Refresh list
|
|
const updated = await developerService.listKeys();
|
|
setKeys(updated);
|
|
addToast('API Key revoked', 'info');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (confirm('Are you sure you want to delete this key permanently?')) {
|
|
await developerService.deleteKey(id);
|
|
setKeys(keys.filter((k) => k.id !== id));
|
|
addToast('API Key deleted', 'info');
|
|
}
|
|
};
|
|
|
|
if (loading)
|
|
return (
|
|
<div className="flex justify-center py-24">
|
|
<Loader2 className="w-10 h-10 text-muted-foreground animate-spin" />
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-8 animate-fadeIn pb-20">
|
|
{/* Header */}
|
|
<div className="flex flex-col md:flex-row justify-between items-end gap-4 border-b border-border/50 pb-6">
|
|
<div>
|
|
<h1 className="text-3xl font-display font-bold text-foreground mb-2">
|
|
DEVELOPER PORTAL
|
|
</h1>
|
|
<p className="text-muted-foreground font-mono text-sm">
|
|
Build on top of the Veza Platform.
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-4">
|
|
<Button
|
|
variant="secondary"
|
|
icon={<ExternalLink className="w-4 h-4" />}
|
|
onClick={() => window.open('https://docs.veza.io', '_blank')}
|
|
>
|
|
Documentation
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
icon={<Plus className="w-4 h-4" />}
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
Create API Key
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="overview" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2 mb-8">
|
|
<TabsTrigger value="overview" className="flex items-center gap-2">
|
|
<Activity className="w-4 h-4" />
|
|
Overview & Keys
|
|
</TabsTrigger>
|
|
<TabsTrigger value="docs" className="flex items-center gap-2">
|
|
<FileText className="w-4 h-4" />
|
|
API Documentation
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview">
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<StatCard
|
|
label="API Requests (24h)"
|
|
value={stats.requests_24h?.toLocaleString() || 0}
|
|
icon={<Activity className="w-5 h-5" />}
|
|
trend={5.2}
|
|
color="cyan"
|
|
/>
|
|
<StatCard
|
|
label="Avg Latency"
|
|
value={`${stats.avg_latency || 0}ms`}
|
|
icon={<Globe className="w-5 h-5" />}
|
|
trend={-12}
|
|
color="lime"
|
|
/>
|
|
<StatCard
|
|
label="Active Keys"
|
|
value={keys.length}
|
|
icon={<Key className="w-5 h-5" />}
|
|
color="gold"
|
|
/>
|
|
</div>
|
|
|
|
{/* API Keys List */}
|
|
<Card variant="default">
|
|
<div className="flex items-center justify-between mb-6 pb-4 border-b border-border/30">
|
|
<h3 className="font-bold text-foreground flex items-center gap-2">
|
|
<Key className="w-4 h-4 text-warning" /> Active API Keys
|
|
</h3>
|
|
<span className="text-xs text-muted-foreground font-mono">
|
|
{keys.filter(k => k.status === 'active').length} active
|
|
</span>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left">
|
|
<thead>
|
|
<tr className="text-xs text-muted-foreground uppercase border-b border-border/50 tracking-wider">
|
|
<th className="pb-3 pl-4">Name</th>
|
|
<th className="pb-3">Key Prefix</th>
|
|
<th className="pb-3">Status</th>
|
|
<th className="pb-3">Created</th>
|
|
<th className="pb-3">Last Used</th>
|
|
<th className="pb-3 text-right pr-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="text-sm">
|
|
{keys.map((key) => (
|
|
<tr
|
|
key={key.id}
|
|
className="border-b border-border/20 hover:bg-foreground/5 transition-colors group"
|
|
>
|
|
<td className="py-4 pl-4 font-bold text-foreground">{key.name}</td>
|
|
<td className="py-4">
|
|
<div className="flex items-center gap-2">
|
|
<code className="font-mono text-warning bg-warning/10 px-2 py-0.5 rounded text-xs">
|
|
{key.prefix}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleCopyKey(key)}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
|
title="Copy key prefix"
|
|
>
|
|
{copiedKeyId === key.id ? (
|
|
<Check className="w-3.5 h-3.5 text-success" />
|
|
) : (
|
|
<Copy className="w-3.5 h-3.5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td className="py-4">
|
|
<span className={`inline-flex items-center gap-1.5 text-xs font-bold uppercase px-2 py-0.5 rounded ${
|
|
key.status === 'active'
|
|
? 'bg-success/15 text-success'
|
|
: 'bg-destructive/15 text-destructive'
|
|
}`}>
|
|
<span className={`w-1.5 h-1.5 rounded-full ${
|
|
key.status === 'active' ? 'bg-success animate-pulse' : 'bg-destructive'
|
|
}`} />
|
|
{key.status}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 text-muted-foreground">{key.created}</td>
|
|
<td className="py-4 text-foreground">{key.lastUsed}</td>
|
|
<td className="py-4 text-right pr-4 flex justify-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
onClick={() => addToast('Full key hidden for security')}
|
|
title="View Key"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
{key.status === 'active' ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:bg-destructive/10"
|
|
onClick={() => handleRevoke(key.id)}
|
|
title="Revoke Key"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-foreground/10"
|
|
onClick={() => handleDelete(key.id)}
|
|
title="Delete Permanently"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{keys.length === 0 && (
|
|
<tr>
|
|
<td colSpan={6} className="py-8 text-center text-muted-foreground">
|
|
No active API keys. Create one to get started.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="docs">
|
|
<Card variant="default" className="p-0 overflow-hidden bg-background/80">
|
|
{/* Use iframe mode for better compatibility if JSON fetching has issues CORS/Auth */}
|
|
<SwaggerUIDoc useIframe={false} />
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{showCreateModal && (
|
|
<CreateAPIKeyModal
|
|
onClose={() => setShowCreateModal(false)}
|
|
onCreate={handleCreateKey}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|