diff --git a/apps/web/package.json b/apps/web/package.json index 49df51f..b2b916b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ "@xyflow/react": "^12.10.0", "bcryptjs": "^3.0.3", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "ioredis": "^5.9.2", "lucide-react": "^0.469.0", "next": "^15.1.0", diff --git a/apps/web/src/app/api/demo/seed/route.ts b/apps/web/src/app/api/demo/seed/route.ts new file mode 100644 index 0000000..f4f8c4b --- /dev/null +++ b/apps/web/src/app/api/demo/seed/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; +import { seedDemoData } from "@/lib/demo-data"; + +export async function POST() { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { demoSeeded: true }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + if (user.demoSeeded) { + return NextResponse.json({ error: "Demo data already seeded" }, { status: 409 }); + } + + await seedDemoData(session.user.id); + + return NextResponse.json({ success: true }, { status: 200 }); + } catch (error) { + console.error("Error seeding demo data:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/apps/web/src/app/dashboard/keys/page.tsx b/apps/web/src/app/dashboard/keys/page.tsx index 2cdf260..020d003 100644 --- a/apps/web/src/app/dashboard/keys/page.tsx +++ b/apps/web/src/app/dashboard/keys/page.tsx @@ -12,6 +12,7 @@ import { Shield, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useKeyboardNav } from "@/hooks/use-keyboard-nav"; interface ApiKey { id: string; @@ -38,6 +39,21 @@ export default function ApiKeysPage() { const [revokingId, setRevokingId] = useState(null); const [confirmRevokeId, setConfirmRevokeId] = useState(null); + const handleKeySelect = useCallback( + (index: number) => { + const key = keys[index]; + if (key) { + setConfirmRevokeId(key.id); + } + }, + [keys] + ); + + const { selectedIndex } = useKeyboardNav({ + itemCount: keys.length, + onSelect: handleKeySelect, + }); + const fetchKeys = useCallback(async () => { setIsLoading(true); try { @@ -157,6 +173,7 @@ export default function ApiKeysPage() { onClick={() => copyToClipboard(newlyCreatedKey.key, "new-key") } + aria-label="Copy API key to clipboard" className={cn( "p-3 rounded-lg border transition-all shrink-0", copiedField === "new-key" @@ -196,10 +213,11 @@ export default function ApiKeysPage() {

Create New API Key

-
) : (
- {keys.map((apiKey) => ( + {keys.map((apiKey, index) => (
diff --git a/apps/web/src/app/dashboard/layout.tsx b/apps/web/src/app/dashboard/layout.tsx index 69481f9..306336d 100644 --- a/apps/web/src/app/dashboard/layout.tsx +++ b/apps/web/src/app/dashboard/layout.tsx @@ -16,6 +16,8 @@ import { Loader2, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { CommandPalette } from "@/components/command-palette"; +import { KeyboardShortcutsHelp, ShortcutsHint } from "@/components/keyboard-shortcuts-help"; interface NavItem { href: string; @@ -151,6 +153,7 @@ function VerificationBanner() {
}> - - + + {allTracesAreDemo && } + Loading traces...
}> + + + ); } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 98232b9..d7f33fe 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -36,3 +36,39 @@ --font-mono: var(--font-jetbrains), 'JetBrains Mono', 'Fira Code', monospace; } } + +[data-animate="hidden"] { + opacity: 0; + transform: translateY(24px); + transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); +} + +[data-animate="visible"] { + opacity: 1; + transform: translateY(0); +} + +[data-animate="hidden"][style*="animation-delay"] { + transition-delay: inherit; +} + +@media (prefers-reduced-motion: reduce) { + [data-animate="hidden"] { + opacity: 1; + transform: none; + transition: none; + } +} + +a:focus-visible, +button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible, +[role="button"]:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 4px; +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index cddd0de..fed6176 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -74,6 +74,12 @@ export default function RootLayout({ return ( + + Skip to content + {children} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index ed1071a..7c04d73 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -17,10 +17,11 @@ import { Clipboard, Shield, } from "lucide-react"; +import { AnimateOnScroll } from "@/components/animate-on-scroll"; export default function HomePage() { return ( -
+