diff --git a/apps/web/package.json b/apps/web/package.json index 44caf7b..72ec1b3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,6 +21,10 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "zod": "^3.24.1" }, "devDependencies": { diff --git a/apps/web/src/app/api/sync/changes/route.ts b/apps/web/src/app/api/sync/changes/route.ts index 77b04c0..2889b9b 100644 --- a/apps/web/src/app/api/sync/changes/route.ts +++ b/apps/web/src/app/api/sync/changes/route.ts @@ -1,5 +1,5 @@ import { prisma } from '@tpmjs/db'; -import { fetchChanges, fetchLatestPackageVersion } from '@tpmjs/npm-client'; +import { fetchChanges, fetchLatestPackageWithMetadata } from '@tpmjs/npm-client'; import { validateTpmjsField } from '@tpmjs/types/tpmjs'; import { type NextRequest, NextResponse } from 'next/server'; import { env } from '~/env'; @@ -51,8 +51,8 @@ export async function POST(request: NextRequest) { // Process each change for (const change of changesResult.results) { try { - // Fetch full package metadata - const pkg = await fetchLatestPackageVersion(change.id); + // Fetch full package metadata with README + const pkg = await fetchLatestPackageWithMetadata(change.id); // Skip if package not found if (!pkg) { @@ -100,6 +100,10 @@ export async function POST(request: NextRequest) { npmRepository: pkg.repository ?? undefined, npmHomepage: pkg.homepage ?? undefined, npmLicense: pkg.license ?? undefined, + npmKeywords: pkg.topLevelKeywords || pkg.keywords || [], + npmReadme: pkg.readme ?? undefined, + npmAuthor: pkg.author ?? undefined, + npmMaintainers: pkg.maintainers ?? undefined, category: tpmjsData.category, description: tpmjsData.description, example: tpmjsData.example, diff --git a/apps/web/src/app/api/sync/keyword/route.ts b/apps/web/src/app/api/sync/keyword/route.ts index 55f50a5..09a64b7 100644 --- a/apps/web/src/app/api/sync/keyword/route.ts +++ b/apps/web/src/app/api/sync/keyword/route.ts @@ -1,5 +1,5 @@ import { prisma } from '@tpmjs/db'; -import { fetchLatestPackageVersion, searchByKeyword } from '@tpmjs/npm-client'; +import { fetchLatestPackageWithMetadata, searchByKeyword } from '@tpmjs/npm-client'; import { validateTpmjsField } from '@tpmjs/types/tpmjs'; import { type NextRequest, NextResponse } from 'next/server'; import { env } from '~/env'; @@ -41,8 +41,8 @@ export async function POST(request: NextRequest) { // Process each package for (const result of searchResults) { try { - // Fetch full package metadata - const pkg = await fetchLatestPackageVersion(result.package.name); + // Fetch full package metadata with README + const pkg = await fetchLatestPackageWithMetadata(result.package.name); // Skip if package not found if (!pkg) { @@ -90,6 +90,10 @@ export async function POST(request: NextRequest) { npmRepository: pkg.repository ?? undefined, npmHomepage: pkg.homepage ?? undefined, npmLicense: pkg.license ?? undefined, + npmKeywords: pkg.topLevelKeywords || pkg.keywords || [], + npmReadme: pkg.readme ?? undefined, + npmAuthor: pkg.author ?? undefined, + npmMaintainers: pkg.maintainers ?? undefined, category: tpmjsData.category, description: tpmjsData.description, example: tpmjsData.example, diff --git a/apps/web/src/app/tool/[slug]/page.tsx b/apps/web/src/app/tool/[slug]/page.tsx index 4ee21ca..9a8a9f2 100644 --- a/apps/web/src/app/tool/[slug]/page.tsx +++ b/apps/web/src/app/tool/[slug]/page.tsx @@ -9,7 +9,9 @@ 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'; +import { useEffect, useState } from 'react'; +import { Markdown } from '../../../components/Markdown'; +import { ThemeToggle } from '../../../components/ThemeToggle'; interface Tool { id: string; @@ -23,6 +25,10 @@ interface Tool { isOfficial: boolean; npmDownloadsLastMonth: number; npmDownloadsLastWeek: number; + npmKeywords: string[]; + npmReadme: string | null; + npmAuthor: { name: string; email?: string; url?: string } | string | null; + npmMaintainers: Array<{ name: string; email?: string }> | null; tpmjsMetadata: { example?: string; parameters?: Array<{ @@ -57,11 +63,11 @@ interface Tool { } | null; githubStars: number | null; npmLicense: string | null; - npmKeywords: string[]; createdAt: string; updatedAt: string; } +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex UI component with many conditional renders export default function ToolDetailPage({ params, }: { @@ -102,575 +108,447 @@ export default function ToolDetailPage({ }, [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...' - ) - ), - ]); + return ( +
{error || 'Tool not found'}
+ + + +{tool.description}
+ {authorName && ( ++ by {authorName} +
+ )} ++ {tool.tpmjsMetadata.aiAgent.useCase} +
++ {tool.tpmjsMetadata.aiAgent.limitations} +
+{param.name}
+ {param.required ? (
+ {param.type}
+ {param.description}
+ {param.default !== undefined && ( +{JSON.stringify(param.default)}
+ Downloads/month
++ {tool.npmDownloadsLastMonth.toLocaleString()} +
+GitHub Stars
++ {tool.githubStars.toLocaleString()} +
+Quality Score
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ // Make links open in new tab
+ a: ({ href, children, ...props }) => {
+ const isExternal = href?.startsWith('http');
+ return (
+
+ {children}
+
+ );
+ },
+ }}
+ >
+ {content}
+