feat(web): add comprehensive tool detail pages and API routes
- Add /api/tools/[slug] endpoint to fetch individual tool by package name - Create dynamic /tool/[slug] detail pages with complete tool information - Display installation commands, usage examples, parameters, and stats - Add breadcrumb navigation and links between search and detail pages - Update tool search page to link to detail pages - Show AI agent integration info, frameworks, tags, and quality scores 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
66dac336b0
commit
b0375057ca
4 changed files with 736 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -42,3 +42,4 @@ storybook-static
|
|||
# changesets
|
||||
.changeset/*.md
|
||||
!.changeset/README.md
|
||||
.vercel
|
||||
|
|
|
|||
51
apps/web/src/app/api/tools/[slug]/route.ts
Normal file
51
apps/web/src/app/api/tools/[slug]/route.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { prisma } from '@tpmjs/db';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/tools/[slug]
|
||||
*
|
||||
* Fetch a single tool by its NPM package name (slug)
|
||||
*/
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
|
||||
// Find the tool by npmPackageName
|
||||
const tool = await prisma.tool.findUnique({
|
||||
where: {
|
||||
npmPackageName: decodeURIComponent(slug),
|
||||
},
|
||||
});
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Return the tool data
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: tool,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching tool:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch tool',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
676
apps/web/src/app/tool/[slug]/page.tsx
Normal file
676
apps/web/src/app/tool/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
|
||||
import { Container } from '@tpmjs/ui/Container/Container';
|
||||
import { Header } from '@tpmjs/ui/Header/Header';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { ProgressBar } from '@tpmjs/ui/ProgressBar/ProgressBar';
|
||||
import Link from 'next/link';
|
||||
import { createElement, useEffect, useState } from 'react';
|
||||
|
||||
interface Tool {
|
||||
id: string;
|
||||
npmPackageName: string;
|
||||
npmVersion: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
npmRepository: { url: string; type: string } | null;
|
||||
qualityScore: string;
|
||||
isOfficial: boolean;
|
||||
npmDownloadsLastMonth: number;
|
||||
npmDownloadsLastWeek: number;
|
||||
tpmjsMetadata: {
|
||||
example?: string;
|
||||
parameters?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
default?: unknown;
|
||||
}>;
|
||||
returns?: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
authentication?: {
|
||||
required: boolean;
|
||||
type?: string;
|
||||
};
|
||||
pricing?: {
|
||||
model: string;
|
||||
};
|
||||
frameworks?: string[];
|
||||
links?: {
|
||||
documentation?: string;
|
||||
repository?: string;
|
||||
homepage?: string;
|
||||
};
|
||||
aiAgent?: {
|
||||
useCase?: string;
|
||||
limitations?: string;
|
||||
examples?: string[];
|
||||
};
|
||||
} | null;
|
||||
githubStars: number | null;
|
||||
npmLicense: string | null;
|
||||
npmKeywords: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function ToolDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): React.ReactElement {
|
||||
const [tool, setTool] = useState<Tool | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [slug, setSlug] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
params.then((p) => setSlug(p.slug));
|
||||
}, [params]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
|
||||
const fetchTool = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/tools/${encodeURIComponent(slug)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setTool(data.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(data.error || 'Failed to fetch tool');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTool();
|
||||
}, [slug]);
|
||||
|
||||
if (loading) {
|
||||
return createElement('div', { className: 'min-h-screen bg-background' }, [
|
||||
createElement(Header, {
|
||||
key: 'header',
|
||||
title: createElement('div', { className: 'flex items-center gap-2' }, [
|
||||
createElement(Link, { key: 'link', href: '/', className: 'flex items-center gap-2' }, [
|
||||
createElement('span', { key: 'title', className: 'text-2xl font-bold' }, 'TPMJS'),
|
||||
createElement(Badge, { key: 'badge', variant: 'outline', size: 'sm' }, 'Beta'),
|
||||
]),
|
||||
]),
|
||||
sticky: true,
|
||||
size: 'md',
|
||||
}),
|
||||
createElement(
|
||||
Container,
|
||||
{ key: 'container', size: 'xl', padding: 'md', className: 'py-12' },
|
||||
createElement(
|
||||
'div',
|
||||
{ className: 'text-center text-foreground-secondary' },
|
||||
'Loading tool...'
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (error || !tool) {
|
||||
return createElement('div', { className: 'min-h-screen bg-background' }, [
|
||||
createElement(Header, {
|
||||
key: 'header',
|
||||
title: createElement('div', { className: 'flex items-center gap-2' }, [
|
||||
createElement(Link, { key: 'link', href: '/', className: 'flex items-center gap-2' }, [
|
||||
createElement('span', { key: 'title', className: 'text-2xl font-bold' }, 'TPMJS'),
|
||||
createElement(Badge, { key: 'badge', variant: 'outline', size: 'sm' }, 'Beta'),
|
||||
]),
|
||||
]),
|
||||
sticky: true,
|
||||
size: 'md',
|
||||
}),
|
||||
createElement(
|
||||
Container,
|
||||
{ key: 'container', size: 'xl', padding: 'md', className: 'py-12' },
|
||||
createElement(
|
||||
'div',
|
||||
{ className: 'text-center' },
|
||||
createElement('p', { className: 'text-red-500 text-lg mb-4' }, error || 'Tool not found'),
|
||||
createElement(
|
||||
Link,
|
||||
{ href: '/tool/tool-search' },
|
||||
createElement(Button, { variant: 'default' }, 'Browse All Tools')
|
||||
)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return createElement('div', { className: 'min-h-screen bg-background' }, [
|
||||
// Header
|
||||
createElement(Header, {
|
||||
key: 'header',
|
||||
title: createElement('div', { className: 'flex items-center gap-2' }, [
|
||||
createElement(Link, { key: 'link', href: '/', className: 'flex items-center gap-2' }, [
|
||||
createElement('span', { key: 'title', className: 'text-2xl font-bold' }, 'TPMJS'),
|
||||
createElement(Badge, { key: 'badge', variant: 'outline', size: 'sm' }, 'Beta'),
|
||||
]),
|
||||
]),
|
||||
actions: createElement('div', { className: 'flex items-center gap-3' }, [
|
||||
createElement(
|
||||
Link,
|
||||
{ key: 'browse', href: '/tool/tool-search' },
|
||||
createElement(Button, { variant: 'ghost', size: 'sm' }, 'Browse Tools')
|
||||
),
|
||||
tool.npmRepository
|
||||
? createElement(
|
||||
'a',
|
||||
{
|
||||
key: 'github',
|
||||
href: tool.npmRepository.url.replace('git+', '').replace('.git', ''),
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'text-foreground-secondary hover:text-foreground transition-colors',
|
||||
},
|
||||
createElement(Icon, { icon: 'github', size: 'md' })
|
||||
)
|
||||
: null,
|
||||
]),
|
||||
sticky: true,
|
||||
size: 'md',
|
||||
}),
|
||||
|
||||
// Main content
|
||||
createElement(Container, { key: 'container', size: 'xl', padding: 'md', className: 'py-8' }, [
|
||||
// Breadcrumb
|
||||
createElement(
|
||||
'div',
|
||||
{
|
||||
key: 'breadcrumb',
|
||||
className: 'flex items-center gap-2 text-sm text-foreground-secondary mb-6',
|
||||
},
|
||||
[
|
||||
createElement(
|
||||
Link,
|
||||
{ key: 'home', href: '/', className: 'hover:text-foreground' },
|
||||
'Home'
|
||||
),
|
||||
createElement('span', { key: 'sep1' }, '/'),
|
||||
createElement(
|
||||
Link,
|
||||
{ key: 'tools', href: '/tool/tool-search', className: 'hover:text-foreground' },
|
||||
'Tools'
|
||||
),
|
||||
createElement('span', { key: 'sep2' }, '/'),
|
||||
createElement(
|
||||
'span',
|
||||
{ key: 'current', className: 'text-foreground' },
|
||||
tool.npmPackageName
|
||||
),
|
||||
]
|
||||
),
|
||||
|
||||
// Title section
|
||||
createElement('div', { key: 'title-section', className: 'mb-8' }, [
|
||||
createElement(
|
||||
'div',
|
||||
{ key: 'title-row', className: 'flex items-start justify-between mb-4' },
|
||||
[
|
||||
createElement('div', { key: 'title-content' }, [
|
||||
createElement(
|
||||
'h1',
|
||||
{ key: 'title', className: 'text-4xl font-bold text-foreground mb-2' },
|
||||
tool.npmPackageName
|
||||
),
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'description', className: 'text-lg text-foreground-secondary' },
|
||||
tool.description
|
||||
),
|
||||
]),
|
||||
tool.isOfficial
|
||||
? createElement(
|
||||
Badge,
|
||||
{ key: 'official', variant: 'default', size: 'lg' },
|
||||
'Official'
|
||||
)
|
||||
: null,
|
||||
]
|
||||
),
|
||||
createElement('div', { key: 'badges', className: 'flex flex-wrap gap-2' }, [
|
||||
createElement(Badge, { key: 'category', variant: 'secondary' }, tool.category),
|
||||
createElement(Badge, { key: 'version', variant: 'outline' }, `v${tool.npmVersion}`),
|
||||
tool.npmLicense
|
||||
? createElement(Badge, { key: 'license', variant: 'outline' }, tool.npmLicense)
|
||||
: null,
|
||||
]),
|
||||
]),
|
||||
|
||||
// Main grid
|
||||
createElement(
|
||||
'div',
|
||||
{ key: 'main-grid', className: 'grid grid-cols-1 lg:grid-cols-3 gap-6' },
|
||||
[
|
||||
// Left column - Main content
|
||||
createElement('div', { key: 'left-col', className: 'lg:col-span-2 space-y-6' }, [
|
||||
// Installation
|
||||
createElement(Card, { key: 'installation' }, [
|
||||
createElement(CardHeader, { key: 'header' }, [
|
||||
createElement(CardTitle, { key: 'title' }, 'Installation'),
|
||||
createElement(
|
||||
CardDescription,
|
||||
{ key: 'desc' },
|
||||
'Install this tool using your preferred package manager'
|
||||
),
|
||||
]),
|
||||
createElement(CardContent, { key: 'content', className: 'space-y-4' }, [
|
||||
createElement(CodeBlock, {
|
||||
key: 'npm',
|
||||
code: `npm install ${tool.npmPackageName}`,
|
||||
language: 'bash',
|
||||
showCopy: true,
|
||||
}),
|
||||
createElement(CodeBlock, {
|
||||
key: 'yarn',
|
||||
code: `yarn add ${tool.npmPackageName}`,
|
||||
language: 'bash',
|
||||
showCopy: true,
|
||||
}),
|
||||
createElement(CodeBlock, {
|
||||
key: 'pnpm',
|
||||
code: `pnpm add ${tool.npmPackageName}`,
|
||||
language: 'bash',
|
||||
showCopy: true,
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Usage Example
|
||||
tool.tpmjsMetadata?.example
|
||||
? createElement(Card, { key: 'example' }, [
|
||||
createElement(CardHeader, { key: 'header' }, [
|
||||
createElement(CardTitle, { key: 'title' }, 'Usage Example'),
|
||||
createElement(CardDescription, { key: 'desc' }, 'Quick start example'),
|
||||
]),
|
||||
createElement(
|
||||
CardContent,
|
||||
{ key: 'content' },
|
||||
createElement(CodeBlock, {
|
||||
code: tool.tpmjsMetadata.example,
|
||||
language: 'typescript',
|
||||
showCopy: true,
|
||||
})
|
||||
),
|
||||
])
|
||||
: null,
|
||||
|
||||
// AI Agent Information
|
||||
tool.tpmjsMetadata?.aiAgent
|
||||
? createElement(Card, { key: 'ai-agent' }, [
|
||||
createElement(CardHeader, { key: 'header' }, [
|
||||
createElement(CardTitle, { key: 'title' }, 'AI Agent Integration'),
|
||||
createElement(
|
||||
CardDescription,
|
||||
{ key: 'desc' },
|
||||
'How AI agents can use this tool'
|
||||
),
|
||||
]),
|
||||
createElement(CardContent, { key: 'content', className: 'space-y-4' }, [
|
||||
tool.tpmjsMetadata.aiAgent.useCase
|
||||
? createElement('div', { key: 'usecase' }, [
|
||||
createElement(
|
||||
'h4',
|
||||
{
|
||||
key: 'title',
|
||||
className: 'text-sm font-semibold text-foreground mb-2',
|
||||
},
|
||||
'Use Case'
|
||||
),
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'text', className: 'text-sm text-foreground-secondary' },
|
||||
tool.tpmjsMetadata.aiAgent.useCase
|
||||
),
|
||||
])
|
||||
: null,
|
||||
tool.tpmjsMetadata.aiAgent.limitations
|
||||
? createElement('div', { key: 'limitations' }, [
|
||||
createElement(
|
||||
'h4',
|
||||
{
|
||||
key: 'title',
|
||||
className: 'text-sm font-semibold text-foreground mb-2',
|
||||
},
|
||||
'Limitations'
|
||||
),
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'text', className: 'text-sm text-foreground-secondary' },
|
||||
tool.tpmjsMetadata.aiAgent.limitations
|
||||
),
|
||||
])
|
||||
: null,
|
||||
tool.tpmjsMetadata.aiAgent.examples &&
|
||||
tool.tpmjsMetadata.aiAgent.examples.length > 0
|
||||
? createElement('div', { key: 'examples' }, [
|
||||
createElement(
|
||||
'h4',
|
||||
{
|
||||
key: 'title',
|
||||
className: 'text-sm font-semibold text-foreground mb-2',
|
||||
},
|
||||
'Examples'
|
||||
),
|
||||
createElement(
|
||||
'ul',
|
||||
{ key: 'list', className: 'list-disc list-inside space-y-1' },
|
||||
tool.tpmjsMetadata.aiAgent.examples.map((example, i) =>
|
||||
createElement(
|
||||
'li',
|
||||
{ key: i, className: 'text-sm text-foreground-secondary' },
|
||||
example
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
: null,
|
||||
]),
|
||||
])
|
||||
: null,
|
||||
|
||||
// Parameters
|
||||
tool.tpmjsMetadata?.parameters && tool.tpmjsMetadata.parameters.length > 0
|
||||
? createElement(Card, { key: 'parameters' }, [
|
||||
createElement(CardHeader, { key: 'header' }, [
|
||||
createElement(CardTitle, { key: 'title' }, 'Parameters'),
|
||||
createElement(
|
||||
CardDescription,
|
||||
{ key: 'desc' },
|
||||
'Available configuration options'
|
||||
),
|
||||
]),
|
||||
createElement(
|
||||
CardContent,
|
||||
{ key: 'content' },
|
||||
createElement(
|
||||
'div',
|
||||
{ className: 'space-y-4' },
|
||||
tool.tpmjsMetadata.parameters.map((param) =>
|
||||
createElement(
|
||||
'div',
|
||||
{
|
||||
key: param.name,
|
||||
className: 'border-b border-border pb-4 last:border-0',
|
||||
},
|
||||
[
|
||||
createElement(
|
||||
'div',
|
||||
{ key: 'header', className: 'flex items-start justify-between mb-2' },
|
||||
[
|
||||
createElement(
|
||||
'code',
|
||||
{ key: 'name', className: 'text-sm font-mono text-foreground' },
|
||||
param.name
|
||||
),
|
||||
param.required
|
||||
? createElement(
|
||||
Badge,
|
||||
{ key: 'required', variant: 'error', size: 'sm' },
|
||||
'Required'
|
||||
)
|
||||
: createElement(
|
||||
Badge,
|
||||
{ key: 'optional', variant: 'outline', size: 'sm' },
|
||||
'Optional'
|
||||
),
|
||||
]
|
||||
),
|
||||
createElement(
|
||||
'div',
|
||||
{ key: 'type', className: 'text-sm text-foreground-secondary mb-1' },
|
||||
[
|
||||
createElement(
|
||||
'span',
|
||||
{ key: 'label', className: 'font-semibold' },
|
||||
'Type: '
|
||||
),
|
||||
createElement(
|
||||
'code',
|
||||
{ key: 'value', className: 'font-mono' },
|
||||
param.type
|
||||
),
|
||||
]
|
||||
),
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'desc', className: 'text-sm text-foreground-secondary' },
|
||||
param.description
|
||||
),
|
||||
param.default !== undefined
|
||||
? createElement(
|
||||
'div',
|
||||
{
|
||||
key: 'default',
|
||||
className: 'text-sm text-foreground-tertiary mt-1',
|
||||
},
|
||||
[
|
||||
createElement(
|
||||
'span',
|
||||
{ key: 'label', className: 'font-semibold' },
|
||||
'Default: '
|
||||
),
|
||||
createElement(
|
||||
'code',
|
||||
{ key: 'value', className: 'font-mono' },
|
||||
JSON.stringify(param.default)
|
||||
),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
: null,
|
||||
]),
|
||||
|
||||
// Right column - Sidebar
|
||||
createElement('div', { key: 'right-col', className: 'space-y-6' }, [
|
||||
// Stats
|
||||
createElement(Card, { key: 'stats' }, [
|
||||
createElement(
|
||||
CardHeader,
|
||||
{ key: 'header' },
|
||||
createElement(CardTitle, { key: 'title' }, 'Statistics')
|
||||
),
|
||||
createElement(CardContent, { key: 'content', className: 'space-y-4' }, [
|
||||
createElement('div', { key: 'downloads' }, [
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'label', className: 'text-sm text-foreground-secondary mb-1' },
|
||||
'Downloads/month'
|
||||
),
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'value', className: 'text-2xl font-bold text-foreground' },
|
||||
tool.npmDownloadsLastMonth.toLocaleString()
|
||||
),
|
||||
]),
|
||||
tool.githubStars !== null
|
||||
? createElement('div', { key: 'stars' }, [
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'label', className: 'text-sm text-foreground-secondary mb-1' },
|
||||
'GitHub Stars'
|
||||
),
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'value', className: 'text-2xl font-bold text-foreground' },
|
||||
tool.githubStars.toLocaleString()
|
||||
),
|
||||
])
|
||||
: null,
|
||||
createElement('div', { key: 'quality' }, [
|
||||
createElement(
|
||||
'p',
|
||||
{ key: 'label', className: 'text-sm text-foreground-secondary mb-2' },
|
||||
'Quality Score'
|
||||
),
|
||||
createElement(ProgressBar, {
|
||||
key: 'bar',
|
||||
value: Number.parseFloat(tool.qualityScore) * 100,
|
||||
variant:
|
||||
Number.parseFloat(tool.qualityScore) >= 0.7
|
||||
? 'success'
|
||||
: Number.parseFloat(tool.qualityScore) >= 0.5
|
||||
? 'primary'
|
||||
: 'warning',
|
||||
size: 'md',
|
||||
showLabel: true,
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Tags
|
||||
tool.tags.length > 0
|
||||
? createElement(Card, { key: 'tags' }, [
|
||||
createElement(
|
||||
CardHeader,
|
||||
{ key: 'header' },
|
||||
createElement(CardTitle, { key: 'title' }, 'Tags')
|
||||
),
|
||||
createElement(
|
||||
CardContent,
|
||||
{ key: 'content' },
|
||||
createElement(
|
||||
'div',
|
||||
{ className: 'flex flex-wrap gap-2' },
|
||||
tool.tags.map((tag) =>
|
||||
createElement(Badge, { key: tag, variant: 'outline', size: 'sm' }, tag)
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
: null,
|
||||
|
||||
// Links
|
||||
createElement(Card, { key: 'links' }, [
|
||||
createElement(
|
||||
CardHeader,
|
||||
{ key: 'header' },
|
||||
createElement(CardTitle, { key: 'title' }, 'Links')
|
||||
),
|
||||
createElement(CardContent, { key: 'content', className: 'space-y-2' }, [
|
||||
createElement(
|
||||
'a',
|
||||
{
|
||||
key: 'npm',
|
||||
href: `https://www.npmjs.com/package/${tool.npmPackageName}`,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className:
|
||||
'flex items-center gap-2 text-sm text-foreground-secondary hover:text-foreground',
|
||||
},
|
||||
[
|
||||
createElement(Icon, { key: 'icon', icon: 'externalLink', size: 'sm' }),
|
||||
createElement('span', { key: 'text' }, 'View on NPM'),
|
||||
]
|
||||
),
|
||||
tool.tpmjsMetadata?.links?.documentation
|
||||
? createElement(
|
||||
'a',
|
||||
{
|
||||
key: 'docs',
|
||||
href: tool.tpmjsMetadata.links.documentation,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className:
|
||||
'flex items-center gap-2 text-sm text-foreground-secondary hover:text-foreground',
|
||||
},
|
||||
[
|
||||
createElement(Icon, { key: 'icon', icon: 'externalLink', size: 'sm' }),
|
||||
createElement('span', { key: 'text' }, 'Documentation'),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
tool.tpmjsMetadata?.links?.repository
|
||||
? createElement(
|
||||
'a',
|
||||
{
|
||||
key: 'repo',
|
||||
href: tool.tpmjsMetadata.links.repository,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className:
|
||||
'flex items-center gap-2 text-sm text-foreground-secondary hover:text-foreground',
|
||||
},
|
||||
[
|
||||
createElement(Icon, { key: 'icon', icon: 'github', size: 'sm' }),
|
||||
createElement('span', { key: 'text' }, 'Repository'),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
tool.tpmjsMetadata?.links?.homepage
|
||||
? createElement(
|
||||
'a',
|
||||
{
|
||||
key: 'home',
|
||||
href: tool.tpmjsMetadata.links.homepage,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className:
|
||||
'flex items-center gap-2 text-sm text-foreground-secondary hover:text-foreground',
|
||||
},
|
||||
[
|
||||
createElement(Icon, { key: 'icon', icon: 'externalLink', size: 'sm' }),
|
||||
createElement('span', { key: 'text' }, 'Homepage'),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
]),
|
||||
]),
|
||||
|
||||
// Frameworks
|
||||
tool.tpmjsMetadata?.frameworks && tool.tpmjsMetadata.frameworks.length > 0
|
||||
? createElement(Card, { key: 'frameworks' }, [
|
||||
createElement(
|
||||
CardHeader,
|
||||
{ key: 'header' },
|
||||
createElement(CardTitle, { key: 'title' }, 'Frameworks')
|
||||
),
|
||||
createElement(
|
||||
CardContent,
|
||||
{ key: 'content' },
|
||||
createElement(
|
||||
'div',
|
||||
{ className: 'flex flex-wrap gap-2' },
|
||||
tool.tpmjsMetadata.frameworks.map((framework) =>
|
||||
createElement(
|
||||
Badge,
|
||||
{ key: framework, variant: 'secondary', size: 'sm' },
|
||||
framework
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
: null,
|
||||
]),
|
||||
]
|
||||
),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { Icon } from '@tpmjs/ui/Icon/Icon';
|
|||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { ProgressBar } from '@tpmjs/ui/ProgressBar/ProgressBar';
|
||||
import { Tabs } from '@tpmjs/ui/Tabs/Tabs';
|
||||
import Link from 'next/link';
|
||||
import { createElement, useEffect, useState } from 'react';
|
||||
|
||||
interface Tool {
|
||||
|
|
@ -322,9 +323,13 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
CardFooter,
|
||||
{ key: 'footer' },
|
||||
createElement(
|
||||
Button,
|
||||
{ variant: 'outline', size: 'sm', className: 'w-full' },
|
||||
'View Details'
|
||||
Link,
|
||||
{ href: `/tool/${encodeURIComponent(tool.npmPackageName)}` },
|
||||
createElement(
|
||||
Button,
|
||||
{ variant: 'outline', size: 'sm', className: 'w-full' },
|
||||
'View Details'
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue