feat: update dashboard and API keys pages to use DashboardLayout

- Convert dashboard overview to use DashboardLayout with sidebar
- Update API keys page with DashboardLayout and table layout
- Add upload icon to icon library
- Consistent sidebar navigation across all dashboard pages
This commit is contained in:
Ajax Davis 2026-01-07 19:03:45 +10:00
parent 1efb9b0003
commit d0e304cdab
3 changed files with 246 additions and 188 deletions

View file

@ -1,108 +1,91 @@
import { AppHeader } from '@/components/AppHeader';
import { SignOutButton } from '@/components/auth/SignOutButton';
import { auth } from '@/lib/auth';
'use client';
import { useSession } from '@/lib/auth-client';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { headers } from 'next/headers';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect('/sign-in');
}
export default function DashboardPage(): React.ReactElement {
const { data: session } = useSession();
return (
<div className="min-h-screen bg-background">
<AppHeader />
<div className="max-w-4xl mx-auto py-12 px-4">
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<SignOutButton />
</div>
{/* Quick Actions */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 mb-8">
<Link href="/dashboard/agents" className="block">
<div className="bg-background border border-border rounded-lg p-6 hover:border-foreground/20 transition-colors group">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon icon="terminal" size="md" className="text-primary" />
</div>
<div>
<h2 className="text-lg font-medium text-foreground">My Agents</h2>
<p className="text-sm text-foreground-secondary">
Create and manage AI agents with tools
</p>
</div>
<DashboardLayout title="Overview">
{/* Quick Actions */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 mb-8">
<Link href="/dashboard/agents" className="block">
<div className="bg-background border border-border rounded-lg p-6 hover:border-foreground/20 transition-colors group">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon icon="terminal" size="md" className="text-primary" />
</div>
<div>
<h2 className="text-lg font-medium text-foreground">My Agents</h2>
<p className="text-sm text-foreground-secondary">
Create and manage AI agents with tools
</p>
</div>
</div>
</Link>
</div>
</Link>
<Link href="/dashboard/collections" className="block">
<div className="bg-background border border-border rounded-lg p-6 hover:border-foreground/20 transition-colors group">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon icon="folder" size="md" className="text-primary" />
</div>
<div>
<h2 className="text-lg font-medium text-foreground">My Collections</h2>
<p className="text-sm text-foreground-secondary">
Organize and share your favorite tools
</p>
</div>
<Link href="/dashboard/collections" className="block">
<div className="bg-background border border-border rounded-lg p-6 hover:border-foreground/20 transition-colors group">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon icon="folder" size="md" className="text-primary" />
</div>
<div>
<h2 className="text-lg font-medium text-foreground">My Collections</h2>
<p className="text-sm text-foreground-secondary">
Organize and share your favorite tools
</p>
</div>
</div>
</Link>
</div>
</Link>
<Link href="/dashboard/settings/api-keys" className="block">
<div className="bg-background border border-border rounded-lg p-6 hover:border-foreground/20 transition-colors group">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon icon="key" size="md" className="text-primary" />
</div>
<div>
<h2 className="text-lg font-medium text-foreground">API Keys</h2>
<p className="text-sm text-foreground-secondary">
Manage AI provider credentials
</p>
</div>
<Link href="/dashboard/settings/api-keys" className="block">
<div className="bg-background border border-border rounded-lg p-6 hover:border-foreground/20 transition-colors group">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon icon="key" size="md" className="text-primary" />
</div>
<div>
<h2 className="text-lg font-medium text-foreground">API Keys</h2>
<p className="text-sm text-foreground-secondary">Manage AI provider credentials</p>
</div>
</div>
</Link>
</div>
</div>
</Link>
</div>
{/* Profile Section */}
<div className="bg-background border border-border rounded-lg p-6">
<h2 className="text-lg font-medium text-foreground mb-4">Profile</h2>
{/* Profile Section */}
<div className="bg-background border border-border rounded-lg p-6">
<h2 className="text-lg font-medium text-foreground mb-4">Profile</h2>
<div className="space-y-4">
<div>
<span className="block text-sm text-foreground-secondary">Name</span>
<p className="text-foreground font-medium">{session.user.name}</p>
</div>
<div className="space-y-4">
<div>
<span className="block text-sm text-foreground-secondary">Name</span>
<p className="text-foreground font-medium">{session?.user?.name || 'User'}</p>
</div>
<div>
<span className="block text-sm text-foreground-secondary">Email</span>
<p className="text-foreground font-medium">{session.user.email}</p>
</div>
<div>
<span className="block text-sm text-foreground-secondary">Email</span>
<p className="text-foreground font-medium">{session?.user?.email}</p>
</div>
<div>
<span className="block text-sm text-foreground-secondary">Email Verified</span>
<p className="text-foreground font-medium">
{session.user.emailVerified ? (
<span className="text-green-600">Verified</span>
) : (
<span className="text-amber-600">Not verified</span>
)}
</p>
</div>
<div>
<span className="block text-sm text-foreground-secondary">Email Verified</span>
<p className="text-foreground font-medium">
{session?.user?.emailVerified ? (
<span className="text-success">Verified</span>
) : (
<span className="text-warning">Not verified</span>
)}
</p>
</div>
</div>
</div>
</div>
</DashboardLayout>
);
}

View file

@ -2,10 +2,18 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import {
Table,
TableBody,
TableCell,
TableEmpty,
TableHead,
TableHeader,
TableRow,
} from '@tpmjs/ui/Table/Table';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { AppHeader } from '~/components/AppHeader';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
interface ApiKeyInfo {
id: string;
@ -15,6 +23,15 @@ interface ApiKeyInfo {
updatedAt: string;
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
export default function ApiKeysPage(): React.ReactElement {
const router = useRouter();
const [keys, setKeys] = useState<ApiKeyInfo[]>([]);
@ -22,6 +39,7 @@ export default function ApiKeysPage(): React.ReactElement {
const [error, setError] = useState<string | null>(null);
// Add new key form
const [showAddForm, setShowAddForm] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [newKeyValue, setNewKeyValue] = useState('');
const [saving, setSaving] = useState(false);
@ -80,6 +98,7 @@ export default function ApiKeysPage(): React.ReactElement {
if (result.success) {
setNewKeyName('');
setNewKeyValue('');
setShowAddForm(false);
fetchKeys();
} else {
setSaveError(result.error || 'Failed to save');
@ -88,7 +107,8 @@ export default function ApiKeysPage(): React.ReactElement {
setSaving(false);
}, [newKeyName, newKeyValue, saveKey, fetchKeys]);
const handleDelete = useCallback(async (keyName: string, id: string) => {
const handleDelete = useCallback(async (keyName: string, id: string, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm(`Delete ${keyName}?`)) return;
setDeletingId(id);
@ -135,137 +155,188 @@ export default function ApiKeysPage(): React.ReactElement {
fetchKeys();
}, [envText, saveKey, fetchKeys]);
if (isLoading) {
return (
<div className="min-h-screen bg-background">
<AppHeader />
<div className="max-w-2xl mx-auto py-12 px-4">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-surface-secondary rounded w-32" />
<div className="h-16 bg-surface-secondary rounded" />
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-background">
<AppHeader />
<div className="max-w-2xl mx-auto py-12 px-4 text-center">
<p className="text-error mb-4">{error}</p>
<Button onClick={fetchKeys}>Retry</Button>
<DashboardLayout title="API Keys">
<div className="text-center py-16">
<Icon icon="alertCircle" size="lg" className="mx-auto text-error mb-4" />
<h2 className="text-lg font-medium text-foreground mb-2">Error</h2>
<p className="text-foreground-secondary mb-4">{error}</p>
<Button onClick={fetchKeys}>Try Again</Button>
</div>
</div>
</DashboardLayout>
);
}
return (
<div className="min-h-screen bg-background">
<AppHeader />
<div className="max-w-2xl mx-auto py-12 px-4">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<Link href="/dashboard" className="text-foreground-secondary hover:text-foreground">
<Icon icon="arrowLeft" size="sm" />
</Link>
<h1 className="text-2xl font-bold text-foreground">Environment Variables</h1>
</div>
<button
type="button"
onClick={() => setShowImport(!showImport)}
className="text-sm text-primary hover:underline"
>
{showImport ? 'Hide' : 'Import .env'}
</button>
</div>
{/* Import from .env */}
{showImport && (
<div className="mb-6 p-4 bg-surface-secondary border border-border rounded-lg">
<textarea
value={envText}
onChange={(e) => setEnvText(e.target.value)}
placeholder="Paste .env content here...
MY_API_KEY=abc123
ANOTHER_SECRET=xyz789"
rows={5}
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground font-mono text-sm placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
/>
<Button
size="sm"
onClick={handleImport}
disabled={importing || !envText.trim()}
className="mt-2"
>
{importing ? 'Importing...' : 'Import'}
<DashboardLayout
title="API Keys"
subtitle={keys.length > 0 ? `${keys.length} key${keys.length !== 1 ? 's' : ''}` : undefined}
actions={
!showAddForm &&
!showImport && (
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setShowImport(true)}>
<Icon icon="upload" size="sm" className="mr-2" />
Import .env
</Button>
<Button onClick={() => setShowAddForm(true)}>
<Icon icon="plus" size="sm" className="mr-2" />
Add Key
</Button>
</div>
)}
)
}
>
{/* Import from .env */}
{showImport && (
<div className="bg-background border border-border rounded-lg p-6 mb-6">
<h2 className="text-lg font-medium text-foreground mb-4">Import from .env</h2>
<textarea
value={envText}
onChange={(e) => setEnvText(e.target.value)}
placeholder="Paste .env content here...
MY_API_KEY=abc123
ANOTHER_SECRET=xyz789"
rows={5}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
/>
<div className="flex items-center gap-2 mt-4">
<Button onClick={handleImport} disabled={importing || !envText.trim()}>
{importing ? 'Importing...' : 'Import Keys'}
</Button>
<Button variant="outline" onClick={() => setShowImport(false)}>
Cancel
</Button>
</div>
</div>
)}
{/* Add new key */}
<div className="mb-6 p-4 bg-surface-secondary border border-border rounded-lg">
<h2 className="text-sm font-medium text-foreground mb-3">Add New Key</h2>
{saveError && <p className="text-red-500 text-sm mb-2">{saveError}</p>}
<div className="flex gap-2 mb-2">
{/* Add new key form */}
{showAddForm && (
<div className="bg-background border border-border rounded-lg p-6 mb-6">
<h2 className="text-lg font-medium text-foreground mb-4">Add New Key</h2>
{saveError && <p className="text-error text-sm mb-3">{saveError}</p>}
<div className="space-y-3">
<input
type="text"
value={newKeyName}
onChange={(e) =>
setNewKeyName(e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, ''))
}
placeholder="KEY_NAME"
className="flex-1 px-3 py-2 bg-background border border-border rounded text-foreground text-sm font-mono placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50"
placeholder="KEY_NAME (e.g., OPENAI_API_KEY)"
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground text-sm font-mono placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div className="flex gap-2">
<input
type="password"
value={newKeyValue}
onChange={(e) => setNewKeyValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddKey()}
placeholder="Value"
className="flex-1 px-3 py-2 bg-background border border-border rounded text-foreground text-sm font-mono placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50"
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground text-sm font-mono placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div className="flex items-center gap-2 mt-4">
<Button
onClick={handleAddKey}
disabled={saving || !newKeyName.trim() || !newKeyValue.trim()}
>
{saving ? 'Saving...' : 'Add'}
{saving ? 'Saving...' : 'Save Key'}
</Button>
<Button variant="outline" onClick={() => setShowAddForm(false)}>
Cancel
</Button>
</div>
</div>
)}
{/* Keys list */}
{keys.length > 0 ? (
<div className="space-y-2">
{keys.map((key) => (
<div
key={key.id}
className="flex items-center justify-between px-4 py-3 bg-surface-secondary border border-border rounded-lg"
>
<div className="flex items-center gap-3">
<span className="font-mono text-sm text-foreground">{key.keyName}</span>
<span className="text-xs text-foreground-tertiary">...{key.keyHint}</span>
</div>
<button
type="button"
onClick={() => handleDelete(key.keyName, key.id)}
disabled={deletingId === key.id}
className="text-foreground-tertiary hover:text-red-500 disabled:opacity-50"
>
<Icon icon="trash" size="sm" />
</button>
</div>
))}
</div>
) : (
<p className="text-center text-foreground-tertiary py-8">
No environment variables stored yet.
</p>
)}
{/* Keys Table */}
<div className="bg-background border border-border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[300px]">Key Name</TableHead>
<TableHead>Value</TableHead>
<TableHead>Added</TableHead>
<TableHead className="w-[80px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<>
{[0, 1, 2].map((idx) => (
<TableRow key={`key-skeleton-${idx}`}>
<TableCell>
<div className="h-4 w-40 bg-surface-secondary rounded animate-pulse" />
</TableCell>
<TableCell>
<div className="h-4 w-20 bg-surface-secondary rounded animate-pulse" />
</TableCell>
<TableCell>
<div className="h-4 w-24 bg-surface-secondary rounded animate-pulse" />
</TableCell>
<TableCell>
<div className="h-8 w-8 bg-surface-secondary rounded animate-pulse ml-auto" />
</TableCell>
</TableRow>
))}
</>
) : keys.length === 0 ? (
<TableEmpty
colSpan={4}
icon={
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
<Icon icon="key" size="lg" className="text-primary" />
</div>
}
title="No API keys yet"
description="Add your AI provider API keys to enable agent functionality. Keys are encrypted and stored securely."
action={
<Button onClick={() => setShowAddForm(true)}>
<Icon icon="plus" size="sm" className="mr-2" />
Add Your First Key
</Button>
}
/>
) : (
keys.map((key) => (
<TableRow key={key.id}>
<TableCell>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<Icon icon="key" size="sm" className="text-primary" />
</div>
<span className="font-mono text-sm text-foreground">{key.keyName}</span>
</div>
</TableCell>
<TableCell>
<span className="text-foreground-tertiary font-mono text-sm">
{key.keyHint}
</span>
</TableCell>
<TableCell>
<span className="text-foreground-secondary text-sm">
{formatDate(key.createdAt)}
</span>
</TableCell>
<TableCell>
<div className="flex items-center justify-end">
<Button
size="sm"
variant="ghost"
onClick={(e) => handleDelete(key.keyName, key.id, e)}
disabled={deletingId === key.id}
>
<Icon icon="trash" size="xs" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</DashboardLayout>
);
}

View file

@ -88,6 +88,10 @@ export const icons = {
viewBox: '0 0 24 24',
path: 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z',
},
upload: {
viewBox: '0 0 24 24',
path: 'M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z',
},
alertCircle: {
viewBox: '0 0 24 24',
path: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z',