From 48c41e873315342da15d2aba62f1d3d4c3f52d4a Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Tue, 20 Jan 2026 16:17:35 +1000 Subject: [PATCH] feat: add use cases marketing product with AI-generated content Transform qualifying scenarios into marketing-ready use cases with: - AI-generated titles, descriptions, ROI estimates, business value - Persona/industry/category taxonomy for targeting - Browseable feed with filtering and ranking - SEO-optimized case study pages - Daily cron job for generation and ranking Database: - Add Persona, Industry, Category lookup tables - Add UseCase model with marketing content fields - Add junction tables for personas/industries/categories - Add SocialProof model for cached metrics API: - GET /api/use-cases - Global directory with filtering - GET /api/use-cases/[id] - Individual use case details - GET /api/public/users/[username]/collections/[slug]/use-cases - POST /api/cron/use-cases - Nightly generation job Frontend: - /use-cases - Global feed with persona dropdown - /use-cases/[slug] - SEO case study page - /[username]/collections/[slug]/use-cases - Collection feed - UseCasesFeed component - Sortable table component - UseCaseCaseStudy component - Full case study layout --- .vscode/settings.json | 3 +- apps/playground/src/app/api/chat/route.ts | 2 +- apps/playground/src/app/layout.tsx | 2 +- apps/railway-executor/server.js | 6 +- apps/railway-executor/server.ts | 2 +- apps/tutorial/src/app/agent-example/page.tsx | 2 +- apps/tutorial/src/app/first-tool/page.tsx | 2 +- apps/tutorial/src/app/globals.css | 6 +- apps/tutorial/src/app/overview/page.tsx | 2 +- apps/tutorial/src/app/playground/page.tsx | 2 +- .../src/components/AgentExampleSlideshow.tsx | 8 +- .../src/components/AgentSlideshow.tsx | 2 +- .../src/components/AuthorSlideshow.tsx | 8 +- .../src/components/FirstToolSlideshow.tsx | 8 +- .../src/components/OverviewSlideshow.tsx | 8 +- .../src/components/PlaygroundSlideshow.tsx | 8 +- apps/tutorial/src/components/Slide.tsx | 2 +- .../src/app/(auth)/forgot-password/page.tsx | 7 +- apps/web/src/app/(auth)/layout.tsx | 2 +- apps/web/src/app/(auth)/sign-in/page.tsx | 11 +- apps/web/src/app/(auth)/sign-up/page.tsx | 6 +- .../collections/[slug]/use-cases/page.tsx | 134 ++++ apps/web/src/app/about/page.tsx | 6 +- apps/web/src/app/agents/page.tsx | 4 +- .../app/api/agents/[id]/collections/route.ts | 2 +- .../src/app/api/agents/[id]/tools/route.ts | 2 +- apps/web/src/app/api/agents/route.ts | 4 +- apps/web/src/app/api/auth/[...all]/route.ts | 2 +- .../[id]/scenarios/generate/route.ts | 2 +- apps/web/src/app/api/cron/use-cases/route.ts | 98 +++ .../collections/[slug]/use-cases/route.ts | 224 ++++++ apps/web/src/app/api/stats/tools/route.ts | 1 - apps/web/src/app/api/sync/changes/route.ts | 2 +- apps/web/src/app/api/tool-ideas/route.ts | 4 +- .../src/app/api/tools/[id]/reviews/route.ts | 23 +- .../app/api/tools/execute/[...slug]/route.ts | 2 +- apps/web/src/app/api/tools/search/route.ts | 7 +- apps/web/src/app/api/tools/trending/route.ts | 1 - apps/web/src/app/api/use-cases/[id]/route.ts | 175 +++++ apps/web/src/app/api/use-cases/route.ts | 219 ++++++ apps/web/src/app/api/user/profile/route.ts | 2 +- apps/web/src/app/cli/auth/page.tsx | 29 +- apps/web/src/app/collections/page.tsx | 6 +- .../agents/[id]/chat/[chatId]/page.tsx | 2 +- .../web/src/app/dashboard/agents/new/page.tsx | 6 +- apps/web/src/app/dashboard/agents/page.tsx | 50 +- .../src/app/dashboard/collections/page.tsx | 50 +- .../src/app/dashboard/likes/agents/page.tsx | 2 +- .../app/dashboard/likes/collections/page.tsx | 2 +- .../src/app/dashboard/likes/tools/page.tsx | 2 +- apps/web/src/app/dashboard/page.tsx | 2 +- .../app/dashboard/settings/api-keys/page.tsx | 36 +- apps/web/src/app/dashboard/usage/page.tsx | 2 +- apps/web/src/app/docs/executors/page.tsx | 3 +- apps/web/src/app/docs/platform-guide/page.tsx | 82 ++- .../docs/tutorials/custom-executor/page.tsx | 4 +- apps/web/src/app/globals.css | 38 +- apps/web/src/app/stats/page.tsx | 5 +- apps/web/src/app/style-guide/page.tsx | 8 +- apps/web/src/app/terms/page.tsx | 4 +- .../src/app/tool-ideas/ToolIdeasClient.tsx | 16 +- .../app/tool/[...slug]/ToolDetailClient.tsx | 4 +- apps/web/src/app/tool/tool-search/page.tsx | 17 +- apps/web/src/app/use-cases/[slug]/page.tsx | 104 +++ apps/web/src/app/use-cases/page.tsx | 67 ++ apps/web/src/components/LikeButton.tsx | 12 +- apps/web/src/components/ToolPlayground.tsx | 6 +- apps/web/src/components/UseCaseCaseStudy.tsx | 319 +++++++++ apps/web/src/components/UseCasesFeed.tsx | 273 ++++++++ .../src/components/agents/ChatToolsPanel.tsx | 88 +-- .../components/agents/ToolDetailsModal.tsx | 6 +- .../web/src/components/auth/SignOutButton.tsx | 2 +- .../components/collections/AddToolSearch.tsx | 73 +- .../src/components/home/FeaturesSection.tsx | 60 +- .../src/components/stats/AnimatedCounter.tsx | 2 +- apps/web/src/components/stats/BarChart.tsx | 4 +- .../style-guide/SectionA11yChecklists.tsx | 58 +- .../style-guide/SectionAccessibility.tsx | 29 +- .../components/style-guide/SectionColors.tsx | 140 +++- .../style-guide/SectionComponentAPIs.tsx | 20 +- .../style-guide/SectionComponents.tsx | 30 +- .../components/style-guide/SectionContent.tsx | 71 +- .../style-guide/SectionContentGuidelines.tsx | 93 ++- .../components/style-guide/SectionDataViz.tsx | 64 +- .../components/style-guide/SectionIcons.tsx | 75 +- .../components/style-guide/SectionLayout.tsx | 56 +- .../components/style-guide/SectionMotion.tsx | 54 +- .../style-guide/SectionPatternFeedback.tsx | 35 +- .../style-guide/SectionPatternForms.tsx | 47 +- .../style-guide/SectionPatternNavigation.tsx | 134 +++- .../style-guide/SectionPatternSearch.tsx | 108 ++- .../style-guide/SectionPatternTables.tsx | 106 ++- .../style-guide/SectionPrinciples.tsx | 14 +- .../components/style-guide/SectionSpacing.tsx | 9 +- .../components/style-guide/SectionTheming.tsx | 10 +- .../style-guide/SectionTypography.tsx | 118 +++- apps/web/src/components/style-guide/index.ts | 66 +- .../web/src/components/style-guide/shared.tsx | 27 +- apps/web/src/hooks/useAgents.ts | 25 +- apps/web/src/hooks/useBundleSize.ts | 11 +- apps/web/src/hooks/useLikeStatus.ts | 5 +- .../lib/ai-agent/tool-executor-agent.test.ts | 2 +- .../lib/health-check/health-check-service.ts | 2 +- apps/web/src/lib/mcp/tool-converter.ts | 2 +- apps/web/src/lib/og/index.ts | 6 +- apps/web/src/lib/use-cases/generate.ts | 657 ++++++++++++++++++ apps/web/src/middleware.ts | 2 +- .../_helpers/setup-test-credentials.ts | 6 +- .../test/integration/_helpers/test-context.ts | 6 +- .../agents-conversations.integration.test.ts | 2 +- apps/web/src/test/setup.ts | 2 +- apps/web/vitest.config.mjs | 2 +- apps/web/vitest.integration.config.mjs | 2 +- manual-tools.ts | 5 +- packages/cli/src/commands/agent/chat.ts | 8 +- packages/cli/src/commands/agent/delete.ts | 2 +- packages/cli/src/commands/auth/status.ts | 11 +- .../cli/src/commands/collection/delete.ts | 2 +- .../cli/src/commands/collection/import.ts | 2 +- .../cli/src/commands/collection/remove.ts | 4 +- packages/cli/src/commands/doctor.ts | 10 +- packages/cli/src/commands/mcp/config.ts | 9 +- packages/cli/src/commands/mcp/serve.ts | 2 +- packages/cli/src/commands/playground.ts | 2 +- packages/cli/src/commands/publish/check.ts | 10 +- packages/cli/src/commands/publish/preview.ts | 2 +- packages/cli/src/commands/scenario/list.ts | 10 +- packages/cli/src/commands/scenario/run.ts | 2 +- packages/cli/src/commands/scenario/test.ts | 4 +- packages/cli/src/commands/tool/execute.ts | 4 +- packages/cli/src/commands/tool/info.ts | 15 +- packages/cli/src/commands/tool/init.ts | 17 +- packages/cli/src/commands/tool/search.ts | 6 +- packages/cli/src/commands/tool/validate.ts | 12 +- packages/cli/src/commands/update.ts | 2 +- packages/cli/src/hooks/init.ts | 2 +- packages/cli/src/index.ts | 39 +- packages/cli/src/lib/api-client.ts | 4 +- packages/cli/src/lib/config.ts | 7 +- packages/cli/src/lib/output.ts | 6 +- packages/cli/tsup.config.ts | 6 +- packages/db/check-tool.mjs | 1 + packages/db/prisma/schema.prisma | 162 +++++ packages/db/src/index.ts | 2 +- packages/mocks/src/handlers.ts | 2 +- packages/npm-client/src/index.ts | 52 +- packages/package-executor/src/index.ts | 2 +- packages/tool-ideas/src/commands/enrich.ts | 4 +- packages/tool-ideas/src/commands/export.ts | 2 +- packages/tool-ideas/src/commands/generate.ts | 4 +- packages/tool-ideas/src/commands/vocab.ts | 10 +- .../src/enrichment/batch-processor.ts | 6 +- .../src/generators/compatibility.ts | 11 +- .../src/generators/skeleton-generator.ts | 8 +- .../tool-ideas/src/generators/vocabulary.ts | 10 +- packages/tool-ideas/src/index.ts | 27 +- .../src/validation/tool-metadata.ts | 2 +- packages/tools/emoji-magic/src/index.ts | 2 +- .../tools/markdown-formatter/src/index.ts | 2 +- .../src/tools/markdownToPlainText.ts | 4 +- .../official/audience-persona/src/index.ts | 4 +- .../content-calendar-plan/src/index.ts | 4 +- .../src/index.ts | 2 +- .../tsup.config.bundled_ko2hafosm0g.mjs | 1 + .../dependency-audit-lite/src/index.ts | 6 +- .../tools/official/faq-from-text/src/index.ts | 2 +- packages/tools/official/hllm/block.ts | 81 +-- packages/tools/official/hllm/src/index.ts | 56 +- .../official/html-to-markdown/src/index.ts | 2 +- .../invoice-data-extract/src/index.ts | 12 +- .../invoice-terms-extract/src/index.ts | 44 +- packages/tools/official/judge/block.ts | 3 +- packages/tools/official/judge/src/index.ts | 150 ++-- .../linear-regression-ols/src/index.ts | 20 +- .../official/markdown-lint-basic/src/index.ts | 2 +- .../official/markdown-to-html/src/index.ts | 2 +- .../onboarding-checklist/src/index.ts | 4 +- .../openapi-snippet-build/src/index.ts | 2 +- packages/tools/official/pivot/src/index.ts | 2 +- .../official/policy-doc-format/src/index.ts | 20 +- .../official/pricing-page-copy/src/index.ts | 2 +- .../official/proposal-outline/src/index.ts | 2 +- .../reconciliation-match/src/index.ts | 10 +- .../official/redact-secrets/src/index.ts | 14 +- packages/tools/official/rss-read/src/index.ts | 2 +- .../official/secret-scan-text/src/index.ts | 12 +- .../official/social-post-draft/src/index.ts | 12 +- .../sprites-checkpoint-create/src/index.ts | 2 +- .../official/tax-deduction-scan/src/index.ts | 4 +- .../official/template-render/src/index.ts | 2 +- .../tools/official/text-chunk/src/index.ts | 6 +- .../official/tos-readability/src/index.ts | 6 +- packages/tools/official/unsandbox/block.ts | 116 ++-- .../tools/official/unsandbox/src/index.ts | 66 +- .../tools/official/url-normalize/src/index.ts | 2 +- packages/tools/unsandbox/src/index.ts | 8 +- packages/ui/src/Accordion/Accordion.tsx | 24 +- packages/ui/src/Accordion/variants.ts | 37 +- packages/ui/src/Breadcrumbs/Breadcrumbs.tsx | 45 +- packages/ui/src/Breadcrumbs/variants.ts | 37 +- packages/ui/src/Card/variants.ts | 17 +- packages/ui/src/Drawer/Drawer.tsx | 82 +-- packages/ui/src/Drawer/variants.ts | 18 +- packages/ui/src/DropdownMenu/DropdownMenu.tsx | 35 +- packages/ui/src/DropdownMenu/variants.ts | 35 +- packages/ui/src/EmptyState/EmptyState.tsx | 27 +- packages/ui/src/ErrorState/ErrorState.tsx | 32 +- packages/ui/src/FormField/variants.ts | 2 +- .../ui/src/InstallSnippet/InstallSnippet.tsx | 13 +- packages/ui/src/InstallSnippet/variants.ts | 37 +- packages/ui/src/LoadingState/LoadingState.tsx | 12 +- packages/ui/src/Modal/Modal.tsx | 82 +-- packages/ui/src/Modal/variants.ts | 12 +- packages/ui/src/PageHeader/PageHeader.tsx | 18 +- packages/ui/src/Pagination/Pagination.tsx | 75 +- packages/ui/src/Pagination/variants.ts | 31 +- packages/ui/src/Popover/Popover.tsx | 23 +- packages/ui/src/Popover/variants.ts | 13 +- packages/ui/src/ProgressBar/ProgressBar.tsx | 2 - packages/ui/src/QualityScore/QualityScore.tsx | 75 +- packages/ui/src/QualityScore/variants.ts | 33 +- packages/ui/src/Radio/Radio.test.tsx | 3 +- packages/ui/src/Radio/variants.ts | 2 +- packages/ui/src/Select/variants.ts | 2 +- packages/ui/src/Skeleton/Skeleton.tsx | 50 +- packages/ui/src/Skeleton/variants.ts | 27 +- packages/ui/src/Slider/Slider.tsx | 2 +- packages/ui/src/Switch/Switch.test.tsx | 3 +- packages/ui/src/Toast/Toast.tsx | 18 +- packages/ui/src/Toast/variants.ts | 17 +- packages/ui/src/ToolCard/ToolCard.tsx | 57 +- packages/ui/src/ToolCard/variants.ts | 37 +- packages/ui/src/Tooltip/Tooltip.tsx | 8 +- packages/ui/src/Tooltip/variants.ts | 7 +- packages/ui/src/system/hooks/useCountUp.ts | 2 +- .../ui/src/system/hooks/useScrollReveal.ts | 2 +- packages/ui/src/tokens/index.ts | 10 +- scripts/backfill-health-checks.ts | 2 +- services/sandbox-executor/server.js | 21 +- sync-hello.ts | 12 +- sync-manual-tools.ts | 3 +- sync-vercel-registry.ts | 2 +- .../vercel-executor/app/execute-tool/route.ts | 2 +- test-schema.ts | 1 + vercel.json | 4 + 245 files changed, 4734 insertions(+), 2075 deletions(-) create mode 100644 apps/web/src/app/(profile)/[username]/collections/[slug]/use-cases/page.tsx create mode 100644 apps/web/src/app/api/cron/use-cases/route.ts create mode 100644 apps/web/src/app/api/public/users/[username]/collections/[slug]/use-cases/route.ts create mode 100644 apps/web/src/app/api/use-cases/[id]/route.ts create mode 100644 apps/web/src/app/api/use-cases/route.ts create mode 100644 apps/web/src/app/use-cases/[slug]/page.tsx create mode 100644 apps/web/src/app/use-cases/page.tsx create mode 100644 apps/web/src/components/UseCaseCaseStudy.tsx create mode 100644 apps/web/src/components/UseCasesFeed.tsx create mode 100644 apps/web/src/lib/use-cases/generate.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f9600c9..3476d20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,8 +2,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "biomejs.biome", "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit" + "source.fixAll.biome": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true diff --git a/apps/playground/src/app/api/chat/route.ts b/apps/playground/src/app/api/chat/route.ts index bd0b07d..1258137 100644 --- a/apps/playground/src/app/api/chat/route.ts +++ b/apps/playground/src/app/api/chat/route.ts @@ -1,6 +1,6 @@ import { createOpenAI } from '@ai-sdk/openai'; import { searchTpmjsToolsTool } from '@tpmjs/search-registry'; -import { type UIMessage, convertToModelMessages, stepCountIs, streamText } from 'ai'; +import { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai'; import type { NextRequest } from 'next/server'; import { env } from '~/env'; import { diff --git a/apps/playground/src/app/layout.tsx b/apps/playground/src/app/layout.tsx index cac4e42..16d4b81 100644 --- a/apps/playground/src/app/layout.tsx +++ b/apps/playground/src/app/layout.tsx @@ -1,7 +1,7 @@ import { Analytics } from '@vercel/analytics/next'; import type { Metadata } from 'next'; -import { ThemeProvider } from 'next-themes'; import { Space_Grotesk, Space_Mono } from 'next/font/google'; +import { ThemeProvider } from 'next-themes'; import './globals.css'; const spaceGrotesk = Space_Grotesk({ diff --git a/apps/railway-executor/server.js b/apps/railway-executor/server.js index b24050b..c58959e 100644 --- a/apps/railway-executor/server.js +++ b/apps/railway-executor/server.js @@ -19,7 +19,7 @@ const moduleCache = new Map(); /** * Health check endpoint */ -app.get('/health', (req, res) => { +app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), @@ -192,7 +192,7 @@ app.post('/execute-tool', async (req, res) => { /** * Clear module cache (for debugging) */ -app.post('/cache/clear', (req, res) => { +app.post('/cache/clear', (_req, res) => { const size = moduleCache.size; moduleCache.clear(); console.log(`🗑️ Cleared cache (${size} entries)`); @@ -206,7 +206,7 @@ app.post('/cache/clear', (req, res) => { /** * Get cache statistics */ -app.get('/cache/stats', (req, res) => { +app.get('/cache/stats', (_req, res) => { const entries = Array.from(moduleCache.keys()); res.json({ diff --git a/apps/railway-executor/server.ts b/apps/railway-executor/server.ts index 486b9e7..5a7389e 100644 --- a/apps/railway-executor/server.ts +++ b/apps/railway-executor/server.ts @@ -947,7 +947,7 @@ async function handler(req: Request): Promise { } // Start server -const port = Number.parseInt(Deno.env.get('PORT') || '3002'); +const port = Number.parseInt(Deno.env.get('PORT') || '3002', 10); console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`); console.log('📦 HTTP imports: ENABLED'); diff --git a/apps/tutorial/src/app/agent-example/page.tsx b/apps/tutorial/src/app/agent-example/page.tsx index 112f1ff..bfc0bac 100644 --- a/apps/tutorial/src/app/agent-example/page.tsx +++ b/apps/tutorial/src/app/agent-example/page.tsx @@ -1,5 +1,5 @@ -import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow'; import type { Metadata } from 'next'; +import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow'; export const metadata: Metadata = { title: 'Real-World Agent Example | TPMJS Tutorials', diff --git a/apps/tutorial/src/app/first-tool/page.tsx b/apps/tutorial/src/app/first-tool/page.tsx index 5f6a007..019a6e4 100644 --- a/apps/tutorial/src/app/first-tool/page.tsx +++ b/apps/tutorial/src/app/first-tool/page.tsx @@ -1,5 +1,5 @@ -import { FirstToolSlideshow } from '@/components/FirstToolSlideshow'; import type { Metadata } from 'next'; +import { FirstToolSlideshow } from '@/components/FirstToolSlideshow'; export const metadata: Metadata = { title: 'Build Your First Tool | TPMJS Tutorials', diff --git a/apps/tutorial/src/app/globals.css b/apps/tutorial/src/app/globals.css index befcf33..559d5e2 100644 --- a/apps/tutorial/src/app/globals.css +++ b/apps/tutorial/src/app/globals.css @@ -139,9 +139,9 @@ *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; + animation-duration: 0.01ms; + animation-iteration-count: 1; + transition-duration: 0.01ms; } } } diff --git a/apps/tutorial/src/app/overview/page.tsx b/apps/tutorial/src/app/overview/page.tsx index 05489b2..a9f7de2 100644 --- a/apps/tutorial/src/app/overview/page.tsx +++ b/apps/tutorial/src/app/overview/page.tsx @@ -1,5 +1,5 @@ -import { OverviewSlideshow } from '@/components/OverviewSlideshow'; import type { Metadata } from 'next'; +import { OverviewSlideshow } from '@/components/OverviewSlideshow'; export const metadata: Metadata = { title: 'Overview | TPMJS Tutorials', diff --git a/apps/tutorial/src/app/playground/page.tsx b/apps/tutorial/src/app/playground/page.tsx index 4dec183..7b4f509 100644 --- a/apps/tutorial/src/app/playground/page.tsx +++ b/apps/tutorial/src/app/playground/page.tsx @@ -1,5 +1,5 @@ -import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow'; import type { Metadata } from 'next'; +import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow'; export const metadata: Metadata = { title: 'Interactive Playground | TPMJS Tutorials', diff --git a/apps/tutorial/src/components/AgentExampleSlideshow.tsx b/apps/tutorial/src/components/AgentExampleSlideshow.tsx index 9a38f3c..f46c8f7 100644 --- a/apps/tutorial/src/components/AgentExampleSlideshow.tsx +++ b/apps/tutorial/src/components/AgentExampleSlideshow.tsx @@ -1,17 +1,17 @@ 'use client'; -import { useSlideshow } from '@/hooks/useSlideshow'; import { AnimatePresence } from 'framer-motion'; import Link from 'next/link'; -import { Slide } from './Slide'; -import { SlideNavigation } from './SlideNavigation'; -import { SlideProgress } from './SlideProgress'; +import { useSlideshow } from '@/hooks/useSlideshow'; import { AgentCodeSlide } from './agent-example-slides/AgentCodeSlide'; import { AgentExampleNextStepsSlide } from './agent-example-slides/AgentExampleNextStepsSlide'; import { AgentExampleWelcomeSlide } from './agent-example-slides/AgentExampleWelcomeSlide'; import { ExecutionFlowSlide } from './agent-example-slides/ExecutionFlowSlide'; import { SetupCodeSlide } from './agent-example-slides/SetupCodeSlide'; import { TheGoalSlide } from './agent-example-slides/TheGoalSlide'; +import { Slide } from './Slide'; +import { SlideNavigation } from './SlideNavigation'; +import { SlideProgress } from './SlideProgress'; const SLIDES = [ { id: 'welcome', Component: AgentExampleWelcomeSlide }, diff --git a/apps/tutorial/src/components/AgentSlideshow.tsx b/apps/tutorial/src/components/AgentSlideshow.tsx index dbdcf35..3052575 100644 --- a/apps/tutorial/src/components/AgentSlideshow.tsx +++ b/apps/tutorial/src/components/AgentSlideshow.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useSlideshow } from '@/hooks/useSlideshow'; import { AnimatePresence } from 'framer-motion'; import Link from 'next/link'; +import { useSlideshow } from '@/hooks/useSlideshow'; import { Slide } from './Slide'; import { SlideNavigation } from './SlideNavigation'; import { SlideProgress } from './SlideProgress'; diff --git a/apps/tutorial/src/components/AuthorSlideshow.tsx b/apps/tutorial/src/components/AuthorSlideshow.tsx index 2890cd5..135209c 100644 --- a/apps/tutorial/src/components/AuthorSlideshow.tsx +++ b/apps/tutorial/src/components/AuthorSlideshow.tsx @@ -1,11 +1,8 @@ 'use client'; -import { useSlideshow } from '@/hooks/useSlideshow'; import { AnimatePresence } from 'framer-motion'; import Link from 'next/link'; -import { Slide } from './Slide'; -import { SlideNavigation } from './SlideNavigation'; -import { SlideProgress } from './SlideProgress'; +import { useSlideshow } from '@/hooks/useSlideshow'; import { AuthorNextStepsSlide } from './author-slides/AuthorNextStepsSlide'; import { AuthorWelcomeSlide } from './author-slides/AuthorWelcomeSlide'; import { GetDiscoveredSlide } from './author-slides/GetDiscoveredSlide'; @@ -15,6 +12,9 @@ import { PackageSetupSlide } from './author-slides/PackageSetupSlide'; import { QualityScoringSlide } from './author-slides/QualityScoringSlide'; import { ToolDefinitionSlide } from './author-slides/ToolDefinitionSlide'; import { WhatIsRegistrySlide } from './author-slides/WhatIsRegistrySlide'; +import { Slide } from './Slide'; +import { SlideNavigation } from './SlideNavigation'; +import { SlideProgress } from './SlideProgress'; const SLIDES = [ { id: 'welcome', Component: AuthorWelcomeSlide }, diff --git a/apps/tutorial/src/components/FirstToolSlideshow.tsx b/apps/tutorial/src/components/FirstToolSlideshow.tsx index 7be911a..df204fc 100644 --- a/apps/tutorial/src/components/FirstToolSlideshow.tsx +++ b/apps/tutorial/src/components/FirstToolSlideshow.tsx @@ -1,17 +1,17 @@ 'use client'; -import { useSlideshow } from '@/hooks/useSlideshow'; import { AnimatePresence } from 'framer-motion'; import Link from 'next/link'; -import { Slide } from './Slide'; -import { SlideNavigation } from './SlideNavigation'; -import { SlideProgress } from './SlideProgress'; +import { useSlideshow } from '@/hooks/useSlideshow'; import { FirstToolNextStepsSlide } from './first-tool-slides/FirstToolNextStepsSlide'; import { FirstToolWelcomeSlide } from './first-tool-slides/FirstToolWelcomeSlide'; import { PackageJsonSlide } from './first-tool-slides/PackageJsonSlide'; import { ProjectSetupSlide } from './first-tool-slides/ProjectSetupSlide'; import { PublishSlide } from './first-tool-slides/PublishSlide'; import { WriteToolSlide } from './first-tool-slides/WriteToolSlide'; +import { Slide } from './Slide'; +import { SlideNavigation } from './SlideNavigation'; +import { SlideProgress } from './SlideProgress'; const SLIDES = [ { id: 'welcome', Component: FirstToolWelcomeSlide }, diff --git a/apps/tutorial/src/components/OverviewSlideshow.tsx b/apps/tutorial/src/components/OverviewSlideshow.tsx index 351db52..3f20aca 100644 --- a/apps/tutorial/src/components/OverviewSlideshow.tsx +++ b/apps/tutorial/src/components/OverviewSlideshow.tsx @@ -1,17 +1,17 @@ 'use client'; -import { useSlideshow } from '@/hooks/useSlideshow'; import { AnimatePresence } from 'framer-motion'; import Link from 'next/link'; -import { Slide } from './Slide'; -import { SlideNavigation } from './SlideNavigation'; -import { SlideProgress } from './SlideProgress'; +import { useSlideshow } from '@/hooks/useSlideshow'; import { ArchitectureSlide } from './overview-slides/ArchitectureSlide'; import { EcosystemSlide } from './overview-slides/EcosystemSlide'; import { ExploreSlide } from './overview-slides/ExploreSlide'; import { OverviewWelcomeSlide } from './overview-slides/OverviewWelcomeSlide'; import { UseCasesSlide } from './overview-slides/UseCasesSlide'; import { WhatIsTpmjsSlide } from './overview-slides/WhatIsTpmjsSlide'; +import { Slide } from './Slide'; +import { SlideNavigation } from './SlideNavigation'; +import { SlideProgress } from './SlideProgress'; const SLIDES = [ { id: 'welcome', Component: OverviewWelcomeSlide }, diff --git a/apps/tutorial/src/components/PlaygroundSlideshow.tsx b/apps/tutorial/src/components/PlaygroundSlideshow.tsx index 3489e6f..440c4b1 100644 --- a/apps/tutorial/src/components/PlaygroundSlideshow.tsx +++ b/apps/tutorial/src/components/PlaygroundSlideshow.tsx @@ -1,17 +1,17 @@ 'use client'; -import { useSlideshow } from '@/hooks/useSlideshow'; import { AnimatePresence } from 'framer-motion'; import Link from 'next/link'; -import { Slide } from './Slide'; -import { SlideNavigation } from './SlideNavigation'; -import { SlideProgress } from './SlideProgress'; +import { useSlideshow } from '@/hooks/useSlideshow'; import { FillParametersSlide } from './playground-slides/FillParametersSlide'; import { FindToolSlide } from './playground-slides/FindToolSlide'; import { OpenPlaygroundSlide } from './playground-slides/OpenPlaygroundSlide'; import { PlaygroundNextStepsSlide } from './playground-slides/PlaygroundNextStepsSlide'; import { PlaygroundWelcomeSlide } from './playground-slides/PlaygroundWelcomeSlide'; import { SeeResultsSlide } from './playground-slides/SeeResultsSlide'; +import { Slide } from './Slide'; +import { SlideNavigation } from './SlideNavigation'; +import { SlideProgress } from './SlideProgress'; const SLIDES = [ { id: 'welcome', Component: PlaygroundWelcomeSlide }, diff --git a/apps/tutorial/src/components/Slide.tsx b/apps/tutorial/src/components/Slide.tsx index d9842bf..68fec02 100644 --- a/apps/tutorial/src/components/Slide.tsx +++ b/apps/tutorial/src/components/Slide.tsx @@ -1,6 +1,6 @@ 'use client'; -import { type Variants, motion } from 'framer-motion'; +import { motion, type Variants } from 'framer-motion'; import type { ReactNode } from 'react'; interface SlideProps { diff --git a/apps/web/src/app/(auth)/forgot-password/page.tsx b/apps/web/src/app/(auth)/forgot-password/page.tsx index f7ff704..74710ae 100644 --- a/apps/web/src/app/(auth)/forgot-password/page.tsx +++ b/apps/web/src/app/(auth)/forgot-password/page.tsx @@ -107,12 +107,7 @@ export default function ForgotPasswordPage() { /> - diff --git a/apps/web/src/app/(auth)/layout.tsx b/apps/web/src/app/(auth)/layout.tsx index 322c85d..7b9aa14 100644 --- a/apps/web/src/app/(auth)/layout.tsx +++ b/apps/web/src/app/(auth)/layout.tsx @@ -1,5 +1,5 @@ -import { AuthHeader } from '@/components/AuthHeader'; import type { ReactNode } from 'react'; +import { AuthHeader } from '@/components/AuthHeader'; export default function AuthLayout({ children }: { children: ReactNode }) { return ( diff --git a/apps/web/src/app/(auth)/sign-in/page.tsx b/apps/web/src/app/(auth)/sign-in/page.tsx index ffd8a80..a541c60 100644 --- a/apps/web/src/app/(auth)/sign-in/page.tsx +++ b/apps/web/src/app/(auth)/sign-in/page.tsx @@ -91,7 +91,9 @@ export default function SignInPage() {
- +
- diff --git a/apps/web/src/app/(auth)/sign-up/page.tsx b/apps/web/src/app/(auth)/sign-up/page.tsx index 0e065e1..3dc47e2 100644 --- a/apps/web/src/app/(auth)/sign-up/page.tsx +++ b/apps/web/src/app/(auth)/sign-up/page.tsx @@ -1,10 +1,10 @@ 'use client'; +import { suggestUsername } from '@tpmjs/types/user'; import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; import { Input } from '@tpmjs/ui/Input/Input'; import { Label } from '@tpmjs/ui/Label/Label'; -import { suggestUsername } from '@tpmjs/types/user'; import Link from 'next/link'; import { useCallback, useEffect, useState } from 'react'; import { signUp } from '~/lib/auth-client'; @@ -222,9 +222,7 @@ export default function SignUpPage() { {!checkingUsername && usernameCheck && !usernameCheck.available && - username.length >= 3 && ( - - )} + username.length >= 3 && }
{/* Username availability message */} diff --git a/apps/web/src/app/(profile)/[username]/collections/[slug]/use-cases/page.tsx b/apps/web/src/app/(profile)/[username]/collections/[slug]/use-cases/page.tsx new file mode 100644 index 0000000..821f79b --- /dev/null +++ b/apps/web/src/app/(profile)/[username]/collections/[slug]/use-cases/page.tsx @@ -0,0 +1,134 @@ +/** + * Collection Use Cases Page + * + * Browseable/searchable feed of use cases for a specific collection + */ + +import { prisma } from '@tpmjs/db'; +import { notFound } from 'next/navigation'; +import { Suspense } from 'react'; +import UseCasesFeed from '~/components/UseCasesFeed'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +type Props = { + params: Promise<{ username: string; slug: string }>; + searchParams: Promise<{ persona?: string; search?: string; sort?: string }>; +}; + +export async function generateMetadata({ params }: Props) { + const { username, slug } = await params; + + try { + const collection = await prisma.collection.findFirst({ + where: { + slug, + user: { username }, + }, + select: { + name: true, + description: true, + }, + }); + + if (!collection) { + return { + title: 'Collection Not Found - TPMJS', + }; + } + + return { + title: `Use Cases - ${collection.name} - TPMJS`, + description: collection.description || `Use cases for ${collection.name}`, + }; + } catch { + return { + title: 'Collection Use Cases - TPMJS', + }; + } +} + +export default async function CollectionUseCasesPage({ params, searchParams }: Props) { + const { username, slug } = await params; + const { persona, search, sort } = await searchParams; + + // Verify collection exists and is public + const collection = await prisma.collection.findFirst({ + where: { + slug, + user: { username }, + isPublic: true, + }, + select: { + id: true, + name: true, + slug: true, + description: true, + user: { + select: { + username: true, + }, + }, + }, + }); + + if (!collection) { + notFound(); + } + + return ( +
+ {/* Header */} +
+
+
+

+ + {collection.name} + + {' / '}Use Cases +

+

Use Cases

+

+ Proven workflows from this collection that deliver real business value. +

+
+
+
+ + {/* Main Content */} +
+ }> + + +
+
+ ); +} + +function UseCasesFeedSkeleton() { + return ( +
+
+
+
+
+
+
+
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ ))} +
+
+ ); +} diff --git a/apps/web/src/app/about/page.tsx b/apps/web/src/app/about/page.tsx index 3eabc68..efa38d9 100644 --- a/apps/web/src/app/about/page.tsx +++ b/apps/web/src/app/about/page.tsx @@ -18,9 +18,9 @@ export default function AboutPage(): React.ReactElement {

- TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools. - It was started in 2024 to make it easy for developers to publish and discover tools - that AI agents can use. + TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools. It + was started in 2024 to make it easy for developers to publish and discover tools that AI + agents can use.

Creator

diff --git a/apps/web/src/app/agents/page.tsx b/apps/web/src/app/agents/page.tsx index e395de6..60df41b 100644 --- a/apps/web/src/app/agents/page.tsx +++ b/apps/web/src/app/agents/page.tsx @@ -198,7 +198,9 @@ export default function PublicAgentsPage(): React.ReactElement { ) : ( <> diff --git a/apps/web/src/app/api/agents/[id]/collections/route.ts b/apps/web/src/app/api/agents/[id]/collections/route.ts index 4acd1ce..e045723 100644 --- a/apps/web/src/app/api/agents/[id]/collections/route.ts +++ b/apps/web/src/app/api/agents/[id]/collections/route.ts @@ -1,5 +1,5 @@ import { prisma } from '@tpmjs/db'; -import { AGENT_LIMITS, AddCollectionToAgentSchema } from '@tpmjs/types/agent'; +import { AddCollectionToAgentSchema, AGENT_LIMITS } from '@tpmjs/types/agent'; import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; diff --git a/apps/web/src/app/api/agents/[id]/tools/route.ts b/apps/web/src/app/api/agents/[id]/tools/route.ts index 07fde8f..6c23d27 100644 --- a/apps/web/src/app/api/agents/[id]/tools/route.ts +++ b/apps/web/src/app/api/agents/[id]/tools/route.ts @@ -1,5 +1,5 @@ import { prisma } from '@tpmjs/db'; -import { AGENT_LIMITS, AddToolToAgentSchema } from '@tpmjs/types/agent'; +import { AddToolToAgentSchema, AGENT_LIMITS } from '@tpmjs/types/agent'; import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; diff --git a/apps/web/src/app/api/agents/route.ts b/apps/web/src/app/api/agents/route.ts index 6076801..4d95967 100644 --- a/apps/web/src/app/api/agents/route.ts +++ b/apps/web/src/app/api/agents/route.ts @@ -33,8 +33,8 @@ export async function GET(request: NextRequest): Promise { } const { searchParams } = new URL(request.url); - const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20'), 50); - const offset = Number.parseInt(searchParams.get('offset') || '0'); + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 50); + const offset = Number.parseInt(searchParams.get('offset') || '0', 10); const agents = await prisma.agent.findMany({ where: { userId: authResult.userId }, diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts index 9900a3b..2123d0a 100644 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ b/apps/web/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/lib/auth'; import { toNextJsHandler } from 'better-auth/next-js'; +import { auth } from '@/lib/auth'; export const { GET, POST } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/collections/[id]/scenarios/generate/route.ts b/apps/web/src/app/api/collections/[id]/scenarios/generate/route.ts index e8ff866..5acaccb 100644 --- a/apps/web/src/app/api/collections/[id]/scenarios/generate/route.ts +++ b/apps/web/src/app/api/collections/[id]/scenarios/generate/route.ts @@ -178,7 +178,7 @@ export async function POST(request: NextRequest, context: RouteContext) { name: scenario.name, tags: scenario.tags, }, - ...(similarityResult && similarityResult.hasSimilar + ...(similarityResult?.hasSimilar ? { similarity: { hasSimilar: true, diff --git a/apps/web/src/app/api/cron/use-cases/route.ts b/apps/web/src/app/api/cron/use-cases/route.ts new file mode 100644 index 0000000..2370508 --- /dev/null +++ b/apps/web/src/app/api/cron/use-cases/route.ts @@ -0,0 +1,98 @@ +/** + * Use Cases Generation Cron Endpoint + * + * Triggers the nightly use cases generation process to: + * 1. Find qualifying scenarios (qualityScore >= 0.3, totalRuns >= 1, lastRunStatus = 'pass') + * 2. Generate marketing content via AI + * 3. Create/update UseCase records + * 4. Update SocialProof cache + * 5. Compute rank scores + * + * Schedule: Daily at midnight (configured in vercel.json) + */ + +import { type NextRequest, NextResponse } from 'next/server'; +import { env } from '~/env'; +import { + computeRankScores, + generateUseCasesForQualifyingScenarios, +} from '~/lib/use-cases/generate'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 300; // 5 minutes for AI generation + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + + // 1. Verify cron secret + const authHeader = request.headers.get('authorization'); + const token = authHeader?.replace('Bearer ', ''); + + if (env.CRON_SECRET && token !== env.CRON_SECRET) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); + } + + try { + // 2. Generate use cases for qualifying scenarios + const generationResult = await generateUseCasesForQualifyingScenarios(); + + // 3. Compute rank scores for all use cases + const rankedCount = await computeRankScores(); + + const durationMs = Date.now() - startTime; + + // 4. Log the result + const { prisma } = await import('@tpmjs/db'); + await prisma.syncLog.create({ + data: { + source: 'use-cases-generation', + status: generationResult.errors > 0 ? 'partial' : 'success', + processed: generationResult.created + generationResult.updated, + skipped: generationResult.skipped, + errors: generationResult.errors, + message: `Created: ${generationResult.created}, Updated: ${generationResult.updated}, Skipped: ${generationResult.skipped}, Ranked: ${rankedCount}`, + metadata: { + durationMs, + rankedCount, + errorDetails: generationResult.errorDetails.slice(0, 5), // Keep first 5 errors + }, + }, + }); + + return NextResponse.json({ + success: true, + data: { + created: generationResult.created, + updated: generationResult.updated, + skipped: generationResult.skipped, + errors: generationResult.errors, + ranked: rankedCount, + durationMs, + }, + }); + } catch (error) { + const durationMs = Date.now() - startTime; + + // Log error + const { prisma } = await import('@tpmjs/db'); + await prisma.syncLog.create({ + data: { + source: 'use-cases-generation', + status: 'error', + processed: 0, + skipped: 0, + errors: 1, + message: error instanceof Error ? error.message : 'Unknown error', + metadata: { + durationMs, + }, + }, + }); + + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/public/users/[username]/collections/[slug]/use-cases/route.ts b/apps/web/src/app/api/public/users/[username]/collections/[slug]/use-cases/route.ts new file mode 100644 index 0000000..93860dc --- /dev/null +++ b/apps/web/src/app/api/public/users/[username]/collections/[slug]/use-cases/route.ts @@ -0,0 +1,224 @@ +/** + * Collection Use Cases API + * + * GET /api/public/users/[username]/collections/[slug]/use-cases + * Get all use cases for a specific collection + */ + +import { prisma } from '@tpmjs/db'; +import type { NextRequest } from 'next/server'; + +import { apiForbidden, apiInternalError, apiNotFound, apiSuccess } from '~/lib/api-response'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +type RouteContext = { + params: Promise<{ username: string; slug: string }>; +}; + +/** + * GET /api/public/users/[username]/collections/[slug]/use-cases + * Get all use cases for a specific collection + */ +export async function GET(request: NextRequest, context: RouteContext) { + const requestId = crypto.randomUUID(); + + try { + const { username: rawUsername, slug } = await context.params; + const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername; + + const { searchParams } = new URL(request.url); + + // Parse query parameters + const persona = searchParams.get('persona'); + const industry = searchParams.get('industry'); + const category = searchParams.get('category'); + const searchQuery = searchParams.get('search'); + const sort = (searchParams.get('sort') || 'rank') as 'rank' | 'quality' | 'runs' | 'recent'; + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100); + const offset = Number.parseInt(searchParams.get('offset') || '0', 10); + + // Find the user first + const user = await prisma.user.findUnique({ + where: { username }, + select: { id: true, username: true }, + }); + + if (!user || !user.username) { + return apiNotFound('User', requestId); + } + + // Find the collection by slug belonging to this user + const collection = await prisma.collection.findFirst({ + where: { + slug, + userId: user.id, + }, + select: { + id: true, + slug: true, + name: true, + description: true, + isPublic: true, + }, + }); + + if (!collection) { + return apiNotFound('Collection', requestId); + } + + // Only return if public + if (!collection.isPublic) { + return apiForbidden('This collection is not public', requestId); + } + + // Build where clause for use cases + const where: Record = { + scenario: { + collectionId: collection.id, + }, + }; + + if (persona) { + where.personas = { + some: { + persona: { slug: persona }, + }, + }; + } + + if (industry) { + where.industries = { + some: { + industry: { slug: industry }, + }, + }; + } + + if (category) { + where.categories = { + some: { + category: { slug: category }, + }, + }; + } + + if (searchQuery) { + where.OR = [ + { marketingTitle: { contains: searchQuery, mode: 'insensitive' } }, + { marketingDesc: { contains: searchQuery, mode: 'insensitive' } }, + { businessValue: { contains: searchQuery, mode: 'insensitive' } }, + ]; + } + + // Build order by + const orderBy: { + rankScore?: 'asc' | 'desc'; + socialProof?: { qualityScore?: 'asc' | 'desc'; totalRuns?: 'asc' | 'desc' }; + createdAt?: 'asc' | 'desc'; + } = {}; + if (sort === 'quality') { + orderBy.socialProof = { qualityScore: 'desc' }; + } else if (sort === 'runs') { + orderBy.socialProof = { totalRuns: 'desc' }; + } else if (sort === 'recent') { + orderBy.createdAt = 'desc'; + } else { + orderBy.rankScore = 'desc'; + } + + // Get total count + const total = await prisma.useCase.count({ where }); + + // Fetch use cases + const useCases = await prisma.useCase.findMany({ + where, + include: { + personas: { + select: { + persona: { + select: { + slug: true, + name: true, + }, + }, + }, + }, + industries: { + select: { + industry: { + select: { + slug: true, + name: true, + }, + }, + }, + }, + categories: { + select: { + category: { + select: { + slug: true, + name: true, + type: true, + }, + }, + }, + }, + socialProof: true, + scenario: { + select: { + id: true, + prompt: true, + name: true, + tags: true, + qualityScore: true, + totalRuns: true, + lastRunStatus: true, + collection: { + select: { + id: true, + name: true, + slug: true, + user: { + select: { + username: true, + }, + }, + }, + }, + }, + }, + }, + orderBy, + take: limit, + skip: offset, + }); + + return apiSuccess( + { + collection: { + id: collection.id, + slug: collection.slug, + name: collection.name, + description: collection.description, + }, + useCases, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }, + { requestId } + ); + } catch (error) { + console.error( + '[API Error] GET /api/public/users/[username]/collections/[slug]/use-cases:', + error + ); + return apiInternalError('Failed to fetch use cases', requestId); + } +} diff --git a/apps/web/src/app/api/stats/tools/route.ts b/apps/web/src/app/api/stats/tools/route.ts index 1110aa2..a92a938 100644 --- a/apps/web/src/app/api/stats/tools/route.ts +++ b/apps/web/src/app/api/stats/tools/route.ts @@ -64,7 +64,6 @@ export async function GET(request: NextRequest) { case 'recent': orderBy = [{ createdAt: 'desc' }]; break; - case 'quality': default: orderBy = [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }]; break; diff --git a/apps/web/src/app/api/sync/changes/route.ts b/apps/web/src/app/api/sync/changes/route.ts index 89ac2e5..e83a83a 100644 --- a/apps/web/src/app/api/sync/changes/route.ts +++ b/apps/web/src/app/api/sync/changes/route.ts @@ -1,7 +1,7 @@ import { prisma } from '@tpmjs/db'; import { fetchChanges, fetchLatestPackageWithMetadata } from '@tpmjs/npm-client'; -import { validateTpmjsField } from '@tpmjs/types/tpmjs'; import type { TpmjsToolDefinition } from '@tpmjs/types/tpmjs'; +import { validateTpmjsField } from '@tpmjs/types/tpmjs'; import { type NextRequest, NextResponse } from 'next/server'; import { env } from '~/env'; import { performHealthCheck } from '~/lib/health-check/health-check-service'; diff --git a/apps/web/src/app/api/tool-ideas/route.ts b/apps/web/src/app/api/tool-ideas/route.ts index 5157786..6284b6a 100644 --- a/apps/web/src/app/api/tool-ideas/route.ts +++ b/apps/web/src/app/api/tool-ideas/route.ts @@ -77,8 +77,8 @@ export async function GET(request: NextRequest) { const category = searchParams.get('category') || ''; const minQuality = Number.parseFloat(searchParams.get('minQuality') || '0'); const verb = searchParams.get('verb') || ''; - const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100'), 10000); - const offset = Number.parseInt(searchParams.get('offset') || '0'); + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100', 10), 10000); + const offset = Number.parseInt(searchParams.get('offset') || '0', 10); const data = loadToolIdeas(); let tools = data.tools; diff --git a/apps/web/src/app/api/tools/[id]/reviews/route.ts b/apps/web/src/app/api/tools/[id]/reviews/route.ts index 7fdea32..62ae96b 100644 --- a/apps/web/src/app/api/tools/[id]/reviews/route.ts +++ b/apps/web/src/app/api/tools/[id]/reviews/route.ts @@ -79,7 +79,6 @@ export async function GET( case 'lowest': orderBy = [{ rating: 'asc' }, { createdAt: 'desc' }]; break; - case 'recent': default: orderBy = [{ createdAt: 'desc' }]; } @@ -365,18 +364,18 @@ export async function POST( return NextResponse.json({ success: true, data: { - id: review!.id, - title: review!.title, - content: review!.content, - rating: review!.rating, - helpfulCount: review!.helpfulCount, - createdAt: review!.createdAt.toISOString(), - updatedAt: review!.updatedAt.toISOString(), + id: review?.id, + title: review?.title, + content: review?.content, + rating: review?.rating, + helpfulCount: review?.helpfulCount, + createdAt: review?.createdAt.toISOString(), + updatedAt: review?.updatedAt.toISOString(), user: { - id: review!.user.id, - name: review!.user.name, - image: review!.user.image, - username: review!.user.username, + id: review?.user.id, + name: review?.user.name, + image: review?.user.image, + username: review?.user.username, }, }, meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, diff --git a/apps/web/src/app/api/tools/execute/[...slug]/route.ts b/apps/web/src/app/api/tools/execute/[...slug]/route.ts index dacbb1d..3f1e491 100644 --- a/apps/web/src/app/api/tools/execute/[...slug]/route.ts +++ b/apps/web/src/app/api/tools/execute/[...slug]/route.ts @@ -3,9 +3,9 @@ * Executes TPMJS tools with AI agents and streams real-time progress */ -import { checkRateLimit, getClientIP } from '@/lib/rate-limiter'; import { prisma } from '@tpmjs/db'; import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit, getClientIP } from '@/lib/rate-limiter'; // Use Node.js runtime for SSE streaming export const runtime = 'nodejs'; diff --git a/apps/web/src/app/api/tools/search/route.ts b/apps/web/src/app/api/tools/search/route.ts index b620549..7a88f60 100644 --- a/apps/web/src/app/api/tools/search/route.ts +++ b/apps/web/src/app/api/tools/search/route.ts @@ -1,6 +1,6 @@ import { prisma } from '@tpmjs/db'; import { type NextRequest, NextResponse } from 'next/server'; -import { STRICT_RATE_LIMIT, checkRateLimit } from '~/lib/rate-limit'; +import { checkRateLimit, STRICT_RATE_LIMIT } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -93,7 +93,7 @@ export async function GET(request: NextRequest) { // Accept both 'q' and 'query' parameters for flexibility const query = searchParams.get('q') || searchParams.get('query') || ''; const category = searchParams.get('category'); - const limit = Math.min(Number.parseInt(searchParams.get('limit') || '10'), 100); + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '10', 10), 100); // Parse excludeIds to filter out tools already in user's collection const excludeIdsParam = searchParams.get('excludeIds'); @@ -206,7 +206,8 @@ export async function GET(request: NextRequest) { // Boost for package name match (when user searches by package name like @tpmjs/tools-unsandbox) const packageNameBoost = hasPackageNameMatch(query, tool.package.npmPackageName) ? 50 : 0; - const finalScore = bm25Score + qualityBoost + downloadBoost + exactNameBoost + packageNameBoost; + const finalScore = + bm25Score + qualityBoost + downloadBoost + exactNameBoost + packageNameBoost; return { tool, score: finalScore }; }); diff --git a/apps/web/src/app/api/tools/trending/route.ts b/apps/web/src/app/api/tools/trending/route.ts index 60611bb..cefe154 100644 --- a/apps/web/src/app/api/tools/trending/route.ts +++ b/apps/web/src/app/api/tools/trending/route.ts @@ -65,7 +65,6 @@ export async function GET(request: NextRequest): Promise; + industries: Array<{ industry: { slug: string; name: string; description: string | null } }>; + categories: Array<{ + category: { slug: string; name: string; type: string; description: string | null }; + }>; + socialProof: { + qualityScore: number; + totalRuns: number; + consecutivePasses: number; + lastRunStatus: string | null; + lastRunAt: Date | null; + successRate: number | null; + lastRunAgo: string | null; + } | null; + scenario: { + id: string; + prompt: string; + name: string | null; + description: string | null; + tags: string[]; + qualityScore: number; + totalRuns: number; + lastRunStatus: string | null; + collection: { + id: string; + name: string; + slug: string | null; + user: { + username: string | null; + }; + } | null; + }; +} + +type Props = { + params: Promise<{ id: string }>; +}; + +export async function GET(_request: NextRequest, { params }: Props) { + try { + const { id } = await params; + + // Find by ID or slug + const useCase = await prisma.useCase.findFirst({ + where: { + OR: [{ id }, { slug: id }], + }, + include: { + personas: { + include: { + persona: { + select: { + slug: true, + name: true, + description: true, + icon: true, + }, + }, + }, + }, + industries: { + include: { + industry: { + select: { + slug: true, + name: true, + description: true, + }, + }, + }, + }, + categories: { + include: { + category: { + select: { + slug: true, + name: true, + type: true, + description: true, + }, + }, + }, + }, + socialProof: true, + scenario: { + select: { + id: true, + prompt: true, + name: true, + description: true, + tags: true, + qualityScore: true, + totalRuns: true, + lastRunStatus: true, + collection: { + select: { + id: true, + name: true, + slug: true, + user: { + select: { + username: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!useCase) { + return NextResponse.json({ success: false, error: 'Use case not found' }, { status: 404 }); + } + + // Transform response + const transformedUseCase: UseCaseDetail = { + id: useCase.id, + slug: useCase.slug, + marketingTitle: useCase.marketingTitle, + marketingDesc: useCase.marketingDesc, + roiEstimate: useCase.roiEstimate, + businessValue: useCase.businessValue, + problemStatement: useCase.problemStatement, + solutionNarrative: useCase.solutionNarrative, + rankScore: useCase.rankScore, + createdAt: useCase.createdAt, + lastRegeneratedAt: useCase.lastRegeneratedAt, + personas: useCase.personas, + industries: useCase.industries, + categories: useCase.categories, + socialProof: useCase.socialProof, + scenario: useCase.scenario, + }; + + return NextResponse.json({ + success: true, + data: transformedUseCase, + }); + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/use-cases/route.ts b/apps/web/src/app/api/use-cases/route.ts new file mode 100644 index 0000000..8debe27 --- /dev/null +++ b/apps/web/src/app/api/use-cases/route.ts @@ -0,0 +1,219 @@ +/** + * Use Cases API - Global Directory + * + * GET /api/use-cases - List all use cases with filtering and pagination + */ + +import { prisma } from '@tpmjs/db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +interface UseCaseWithRelations { + id: string; + slug: string; + marketingTitle: string; + marketingDesc: string; + roiEstimate: string | null; + businessValue: string | null; + rankScore: number; + createdAt: Date; + lastRegeneratedAt: Date | null; + personas: Array<{ persona: { slug: string; name: string } }>; + industries: Array<{ industry: { slug: string; name: string } }>; + categories: Array<{ category: { slug: string; name: string; type: string } }>; + socialProof: { + qualityScore: number; + totalRuns: number; + consecutivePasses: number; + lastRunStatus: string | null; + lastRunAt: Date | null; + successRate: number | null; + lastRunAgo: string | null; + } | null; + scenario: { + id: string; + prompt: string; + name: string | null; + tags: string[]; + collection: { + id: string; + name: string; + slug: string | null; + user: { + username: string | null; + }; + } | null; + }; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + + // Parse query parameters + const persona = searchParams.get('persona'); + const industry = searchParams.get('industry'); + const category = searchParams.get('category'); + const search = searchParams.get('search'); + const sort = (searchParams.get('sort') || 'rank') as 'rank' | 'quality' | 'runs' | 'recent'; + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100); + const offset = Number.parseInt(searchParams.get('offset') || '0', 10); + + // Build where clause + const where: Record = {}; + + if (persona) { + where.personas = { + some: { + persona: { slug: persona }, + }, + }; + } + + if (industry) { + where.industries = { + some: { + industry: { slug: industry }, + }, + }; + } + + if (category) { + where.categories = { + some: { + category: { slug: category }, + }, + }; + } + + if (search) { + where.OR = [ + { marketingTitle: { contains: search, mode: 'insensitive' } }, + { marketingDesc: { contains: search, mode: 'insensitive' } }, + { businessValue: { contains: search, mode: 'insensitive' } }, + ]; + } + + // Build order by + const orderBy: { + rankScore?: 'asc' | 'desc'; + socialProof?: { qualityScore?: 'asc' | 'desc'; totalRuns?: 'asc' | 'desc' }; + createdAt?: 'asc' | 'desc'; + } = {}; + if (sort === 'quality') { + orderBy.socialProof = { qualityScore: 'desc' }; + } else if (sort === 'runs') { + orderBy.socialProof = { totalRuns: 'desc' }; + } else if (sort === 'recent') { + orderBy.createdAt = 'desc'; + } else { + orderBy.rankScore = 'desc'; + } + + // Get total count + const total = await prisma.useCase.count({ where }); + + // Fetch use cases + const useCases = await prisma.useCase.findMany({ + where, + include: { + personas: { + select: { + persona: { + select: { + slug: true, + name: true, + }, + }, + }, + }, + industries: { + select: { + industry: { + select: { + slug: true, + name: true, + }, + }, + }, + }, + categories: { + select: { + category: { + select: { + slug: true, + name: true, + type: true, + }, + }, + }, + }, + socialProof: true, + scenario: { + select: { + id: true, + prompt: true, + name: true, + tags: true, + collection: { + select: { + id: true, + name: true, + slug: true, + user: { + select: { + username: true, + }, + }, + }, + }, + }, + }, + }, + orderBy, + take: limit, + skip: offset, + }); + + // Transform response + const transformedUseCases: UseCaseWithRelations[] = useCases.map((uc) => ({ + id: uc.id, + slug: uc.slug, + marketingTitle: uc.marketingTitle, + marketingDesc: uc.marketingDesc, + roiEstimate: uc.roiEstimate, + businessValue: uc.businessValue, + rankScore: uc.rankScore, + createdAt: uc.createdAt, + lastRegeneratedAt: uc.lastRegeneratedAt, + personas: uc.personas, + industries: uc.industries, + categories: uc.categories, + socialProof: uc.socialProof, + scenario: uc.scenario, + })); + + return NextResponse.json({ + success: true, + data: { + useCases: transformedUseCases, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }, + }); + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/user/profile/route.ts b/apps/web/src/app/api/user/profile/route.ts index f559bbe..3eadfe9 100644 --- a/apps/web/src/app/api/user/profile/route.ts +++ b/apps/web/src/app/api/user/profile/route.ts @@ -1,5 +1,5 @@ import { prisma } from '@tpmjs/db'; -import { RESERVED_USERNAMES, USERNAME_REGEX, UpdateUserProfileSchema } from '@tpmjs/types/user'; +import { RESERVED_USERNAMES, UpdateUserProfileSchema, USERNAME_REGEX } from '@tpmjs/types/user'; import { type NextRequest, NextResponse } from 'next/server'; import { authenticateRequest } from '~/lib/api-keys/middleware'; diff --git a/apps/web/src/app/cli/auth/page.tsx b/apps/web/src/app/cli/auth/page.tsx index 9150368..3291eef 100644 --- a/apps/web/src/app/cli/auth/page.tsx +++ b/apps/web/src/app/cli/auth/page.tsx @@ -118,7 +118,10 @@ function CliAuthContent(): React.ReactElement {

Don't have an account?{' '} - + Sign up

@@ -147,23 +150,15 @@ function CliAuthContent(): React.ReactElement {
{user.image ? ( - + ) : (
)}
-

- {user.name || 'User'} -

-

- {user.email} -

+

{user.name || 'User'}

+

{user.email}

@@ -195,11 +190,7 @@ function CliAuthContent(): React.ReactElement { {/* Actions */}
-

- An API key will be created and sent to the CLI. - You can revoke it anytime from your dashboard. + An API key will be created and sent to the CLI. You can revoke it anytime from your + dashboard.

diff --git a/apps/web/src/app/collections/page.tsx b/apps/web/src/app/collections/page.tsx index ef05728..b7348ef 100644 --- a/apps/web/src/app/collections/page.tsx +++ b/apps/web/src/app/collections/page.tsx @@ -242,7 +242,11 @@ export default function PublicCollectionsPage(): React.ReactElement { ) : ( <> diff --git a/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx b/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx index 5d2f927..4c3cc4d 100644 --- a/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx +++ b/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx @@ -360,7 +360,7 @@ export default function AgentChatPage(): React.ReactElement { // Scroll to bottom only when messages change or streaming content updates useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages.length, streamingContent]); + }, []); const handleSelectConversation = (convSlug: string) => { router.push(`/dashboard/agents/${agentId}/chat/${convSlug}`); diff --git a/apps/web/src/app/dashboard/agents/new/page.tsx b/apps/web/src/app/dashboard/agents/new/page.tsx index 67eb6ea..6d70a43 100644 --- a/apps/web/src/app/dashboard/agents/new/page.tsx +++ b/apps/web/src/app/dashboard/agents/new/page.tsx @@ -322,11 +322,7 @@ export default function NewAgentPage(): React.ReactElement { {error && (
- +

{error}

diff --git a/apps/web/src/app/dashboard/agents/page.tsx b/apps/web/src/app/dashboard/agents/page.tsx index d4dcf2a..f5ad70b 100644 --- a/apps/web/src/app/dashboard/agents/page.tsx +++ b/apps/web/src/app/dashboard/agents/page.tsx @@ -168,33 +168,31 @@ export default function AgentsPage(): React.ReactElement { {isLoading ? ( // Loading skeleton - <> - {[0, 1, 2].map((idx) => ( - - -
-
-
-
-
-
+ [0, 1, 2].map((idx) => ( + + +
+
+
+
+
- - -
- - -
- - -
- - -
- - - ))} - +
+
+ +
+ + +
+ + +
+ + +
+ + + )) ) : agents.length === 0 ? ( {isLoading ? ( // Loading skeleton - <> - {[0, 1, 2].map((idx) => ( - - -
-
-
-
-
-
+ [0, 1, 2].map((idx) => ( + + +
+
+
+
+
- - -
- - -
- - -
- - -
- - - ))} - +
+
+ +
+ + +
+ + +
+ + +
+ + + )) ) : collections.length === 0 ? ( {isLoading ? ( - <> - {[0, 1, 2].map((idx) => ( - - -
- - -
- - -
- - -
- - - ))} - + [0, 1, 2].map((idx) => ( + + +
+ + +
+ + +
+ + +
+ + + )) ) : keys.length === 0 ? ( {data.timeSeries.at(-1)?.periodStart && - new Date(data.timeSeries.at(-1)!.periodStart).toLocaleDateString()} + new Date(data.timeSeries.at(-1)?.periodStart).toLocaleDateString()}
diff --git a/apps/web/src/app/docs/executors/page.tsx b/apps/web/src/app/docs/executors/page.tsx index a3da8c0..4080f57 100644 --- a/apps/web/src/app/docs/executors/page.tsx +++ b/apps/web/src/app/docs/executors/page.tsx @@ -214,8 +214,7 @@ export default function ExecutorsDocsPage(): React.ReactElement { {/* GET /health */}

- GET{' '} - /health + GET /health

Check executor health status. Used by TPMJS to verify the executor is reachable. diff --git a/apps/web/src/app/docs/platform-guide/page.tsx b/apps/web/src/app/docs/platform-guide/page.tsx index f80d490..d1b2140 100644 --- a/apps/web/src/app/docs/platform-guide/page.tsx +++ b/apps/web/src/app/docs/platform-guide/page.tsx @@ -478,8 +478,8 @@ export default function PlatformGuidePage(): React.ReactElement { with a complete audit trail

  • - Usage Analytics - Monitor API calls, - tokens, and costs + Usage Analytics - Monitor API + calls, tokens, and costs
  • @@ -792,7 +792,9 @@ Invalid usernames:
  • Windows:{' '} - %APPDATA%\Claude\claude_desktop_config.json + + %APPDATA%\Claude\claude_desktop_config.json +
  • @@ -934,7 +936,8 @@ Invalid usernames: name: 'Temperature', type: 'number', required: false, - description: 'Response randomness (0 = deterministic, 2 = creative). Default: 0.7', + description: + 'Response randomness (0 = deterministic, 2 = creative). Default: 0.7', }, { name: 'Max Tool Calls', @@ -996,7 +999,9 @@ Always cite your sources and be transparent about limitations.`}
    • Maximum 50 individual tools per agent
    • Maximum 10 collections per agent
    • -
    • Tools from attached collections don't count against the 50 tool limit
    • +
    • + Tools from attached collections don't count against the 50 tool limit +
    @@ -1061,7 +1066,11 @@ Always cite your sources and be transparent about limitations.`} { name: 'Anthropic', desc: 'Claude models known for nuanced understanding', - models: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'], + models: [ + 'claude-sonnet-4-20250514', + 'claude-3-5-haiku-20241022', + 'claude-3-opus-20240229', + ], keyUrl: 'https://console.anthropic.com/settings/keys', }, { @@ -1073,7 +1082,11 @@ Always cite your sources and be transparent about limitations.`} { name: 'Groq', desc: 'Ultra-fast inference for open-source models', - models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'], + models: [ + 'llama-3.3-70b-versatile', + 'llama-3.1-8b-instant', + 'mixtral-8x7b-32768', + ], keyUrl: 'https://console.groq.com/keys', }, { @@ -1083,7 +1096,10 @@ Always cite your sources and be transparent about limitations.`} keyUrl: 'https://console.mistral.ai/api-keys', }, ].map((provider) => ( -
    +

    {provider.name}

    - After forking, you can check what the original parent has changed by visiting - its page. This lets you see new tools or collections the parent author has added. + After forking, you can check what the original parent has changed by visiting its + page. This lets you see new tools or collections the parent author has added.

    +

    1. Go to your forked agent or collection in the dashboard

    +

    2. Click the "Forked from" badge to navigate to the parent

    - 1. Go to your forked agent or collection in the dashboard -

    -

    - 2. Click the "Forked from" badge to navigate to the parent -

    -

    - 3. The parent's page shows its current tools and collections -

    -

    - 4. Compare with your fork to see what's different + 3. The parent's page shows its current tools and + collections

    +

    4. Compare with your fork to see what's different

    @@ -1324,18 +1335,10 @@ Always cite your sources and be transparent about limitations.`} the parent. To incorporate parent changes:

    -

    - 1. View the parent's current tools/collections -

    -

    - 2. Go to your fork's edit page -

    -

    - 3. Manually add or remove tools to match (or improve upon) the parent -

    -

    - 4. Update settings as needed -

    +

    1. View the parent's current tools/collections

    +

    2. Go to your fork's edit page

    +

    3. Manually add or remove tools to match (or improve upon) the parent

    +

    4. Update settings as needed

    @@ -1447,9 +1450,9 @@ curl "https://tpmjs.com/api/public/users/{username}/agents/{uid}"

    - Important: Your API key is displayed only - once when created. Store it securely. If you lose it, you'll need to generate - a new one. + Important: Your API key is displayed + only once when created. Store it securely. If you lose it, you'll need to + generate a new one.

    @@ -1508,7 +1511,10 @@ curl "https://tpmjs.com/api/public/users/{username}/agents/{uid}"

    Include your API key in the Authorization header:

    - +

    Example request:

    You can access public agents and collections using your own API key—but you must - provide all required credentials{' '} - (LLM keys, tool environment variables) in the request. The owner's stored + provide all required credentials (LLM + keys, tool environment variables) in the request. The owner's stored credentials are never shared.

    diff --git a/apps/web/src/app/docs/tutorials/custom-executor/page.tsx b/apps/web/src/app/docs/tutorials/custom-executor/page.tsx index 8f7368c..2eb4275 100644 --- a/apps/web/src/app/docs/tutorials/custom-executor/page.tsx +++ b/apps/web/src/app/docs/tutorials/custom-executor/page.tsx @@ -180,9 +180,7 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
    -

    - Security Recommendation -

    +

    Security Recommendation

    Set EXECUTOR_API_KEY to require authentication. Without it, anyone with your executor URL can execute diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index ca52535..f516507 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -155,16 +155,16 @@ =========================================== */ /* Type scale */ - --text-xs: 0.75rem; /* 12px */ - --text-sm: 0.875rem; /* 14px */ - --text-base: 1rem; /* 16px */ - --text-lg: 1.125rem; /* 18px */ - --text-xl: 1.25rem; /* 20px */ - --text-2xl: 1.5rem; /* 24px */ - --text-3xl: 2rem; /* 32px */ - --text-4xl: 2.5rem; /* 40px */ - --text-5xl: 3rem; /* 48px */ - --text-6xl: 4rem; /* 64px */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 2rem; /* 32px */ + --text-4xl: 2.5rem; /* 40px */ + --text-5xl: 3rem; /* 48px */ + --text-6xl: 4rem; /* 64px */ /* Max line widths for readability */ --prose-width: 65ch; @@ -226,10 +226,14 @@ --shadow-none: none; --shadow-xs: 0 1px 2px 0 hsl(var(--foreground) / 0.05); - --shadow-sm: 0 1px 3px 0 hsl(var(--foreground) / 0.08), 0 1px 2px -1px hsl(var(--foreground) / 0.08); - --shadow-md: 0 4px 6px -1px hsl(var(--foreground) / 0.08), 0 2px 4px -2px hsl(var(--foreground) / 0.08); - --shadow-lg: 0 10px 15px -3px hsl(var(--foreground) / 0.08), 0 4px 6px -4px hsl(var(--foreground) / 0.08); - --shadow-xl: 0 20px 25px -5px hsl(var(--foreground) / 0.08), 0 8px 10px -6px hsl(var(--foreground) / 0.08); + --shadow-sm: + 0 1px 3px 0 hsl(var(--foreground) / 0.08), 0 1px 2px -1px hsl(var(--foreground) / 0.08); + --shadow-md: + 0 4px 6px -1px hsl(var(--foreground) / 0.08), 0 2px 4px -2px hsl(var(--foreground) / 0.08); + --shadow-lg: + 0 10px 15px -3px hsl(var(--foreground) / 0.08), 0 4px 6px -4px hsl(var(--foreground) / 0.08); + --shadow-xl: + 0 20px 25px -5px hsl(var(--foreground) / 0.08), 0 8px 10px -6px hsl(var(--foreground) / 0.08); --shadow-2xl: 0 25px 50px -12px hsl(var(--foreground) / 0.15); --shadow-inner: inset 0 2px 4px 0 hsl(var(--foreground) / 0.05); @@ -745,9 +749,9 @@ *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; + animation-duration: 0.01ms; + animation-iteration-count: 1; + transition-duration: 0.01ms; } } diff --git a/apps/web/src/app/stats/page.tsx b/apps/web/src/app/stats/page.tsx index 868999c..881e957 100644 --- a/apps/web/src/app/stats/page.tsx +++ b/apps/web/src/app/stats/page.tsx @@ -194,10 +194,7 @@ export default function StatsPage() { if (error || !stats) { return (

    - +
    ); } diff --git a/apps/web/src/app/style-guide/page.tsx b/apps/web/src/app/style-guide/page.tsx index 1ab6d47..56c1fd3 100644 --- a/apps/web/src/app/style-guide/page.tsx +++ b/apps/web/src/app/style-guide/page.tsx @@ -132,8 +132,12 @@ export default function StyleGuidePage(): React.ReactElement { Design System
    - v2.0 - wcag aa + + v2.0 + + + wcag aa +
    diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx index 2fd6038..c1cda52 100644 --- a/apps/web/src/app/terms/page.tsx +++ b/apps/web/src/app/terms/page.tsx @@ -454,7 +454,9 @@ export default function TermsPage(): React.ReactElement {
    - +
    diff --git a/apps/web/src/app/tool-ideas/ToolIdeasClient.tsx b/apps/web/src/app/tool-ideas/ToolIdeasClient.tsx index 85f7c2c..9b05d93 100644 --- a/apps/web/src/app/tool-ideas/ToolIdeasClient.tsx +++ b/apps/web/src/app/tool-ideas/ToolIdeasClient.tsx @@ -228,7 +228,9 @@ export function ToolIdeasClient() { {/* Filters */}
    - +
    - +
    - +