Skip to content

Commit becf3e3

Browse files
ericyangpanclaude
andcommitted
feat(components): add reusable entity components
Extract and add new standalone components for use across pages: - CommunityLinks: display community/social links (Twitter, Discord, etc.) - ModelBenchmarks: display model benchmark scores - ModelSpecifications: display model technical specifications - PlatformIcons: platform icon helper component - PlatformLinks: display AI platform links (HuggingFace, etc.) - VendorModels: display grid of models by a vendor - VendorProducts: display grid of products by a vendor These components replace the nested entity template sections and can be composed directly by pages using PageLayout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 583eafc commit becf3e3

File tree

7 files changed

+432
-0
lines changed

7 files changed

+432
-0
lines changed

src/components/CommunityLinks.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { LinkCardGrid } from '@/components/product/LinkCard'
2+
import type { ManifestCommunityUrls } from '@/types/manifests'
3+
4+
export interface CommunityLinksProps {
5+
communityUrls: ManifestCommunityUrls | null | undefined
6+
title: string
7+
links: Array<{
8+
key: string
9+
title: string
10+
description: string
11+
}>
12+
layout?: 'horizontal' | 'vertical'
13+
gridCols?: string
14+
}
15+
16+
/**
17+
* CommunityLinks Section
18+
*
19+
* Displays community/social links for organizations (vendors, providers).
20+
* Reuses LinkCardGrid for consistent styling.
21+
*/
22+
export function CommunityLinks({
23+
communityUrls,
24+
title,
25+
links,
26+
layout = 'vertical',
27+
gridCols = 'grid-cols-2 md:grid-cols-4',
28+
}: CommunityLinksProps) {
29+
if (!communityUrls) {
30+
return null
31+
}
32+
33+
return (
34+
<LinkCardGrid
35+
title={title}
36+
links={links}
37+
urls={communityUrls}
38+
layout={layout}
39+
gridCols={gridCols}
40+
/>
41+
)
42+
}

src/components/ModelBenchmarks.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BENCHMARK_KEYS, formatBenchmarkValue, hasBenchmarks } from '@/lib/benchmarks'
2+
import type { ManifestModel } from '@/types/manifests'
3+
4+
export interface ModelBenchmarksProps {
5+
benchmarks: ManifestModel['benchmarks']
6+
translations: {
7+
title: string
8+
[key: string]: string
9+
}
10+
}
11+
12+
/**
13+
* ModelBenchmarks Section
14+
*
15+
* Displays performance benchmark scores for AI models.
16+
*/
17+
export function ModelBenchmarks({ benchmarks, translations }: ModelBenchmarksProps) {
18+
if (!benchmarks || !hasBenchmarks(benchmarks)) {
19+
return null
20+
}
21+
22+
return (
23+
<section className="py-[var(--spacing-lg)] border-b border-[var(--color-border)]">
24+
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
25+
<h2 className="text-2xl font-semibold tracking-[-0.02em] mb-[var(--spacing-sm)]">
26+
{translations.title}
27+
</h2>
28+
29+
<div className="grid grid-cols-1 md:grid-cols-2 gap-[var(--spacing-md)] mt-[var(--spacing-lg)]">
30+
{BENCHMARK_KEYS.map(key => {
31+
const value = benchmarks?.[key]
32+
if (value === null || value === undefined) return null
33+
34+
return (
35+
<div key={key} className="border border-[var(--color-border)] p-[var(--spacing-md)]">
36+
<h3 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium mb-[var(--spacing-xs)]">
37+
{translations[key]}
38+
</h3>
39+
<p className="text-lg font-semibold tracking-tight mb-1">
40+
{formatBenchmarkValue(key, value)}
41+
</p>
42+
<p className="text-xs text-[var(--color-text-muted)]">
43+
{translations[`${key}Desc`]}
44+
</p>
45+
</div>
46+
)
47+
})}
48+
</div>
49+
</div>
50+
</section>
51+
)
52+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { formatTokenCount } from '@/lib/format'
2+
import type { ManifestModel } from '@/types/manifests'
3+
4+
export interface ModelSpecificationsProps {
5+
model: Pick<ManifestModel, 'size' | 'contextWindow' | 'maxOutput' | 'tokenPricing'>
6+
translations: {
7+
title: string
8+
modelSize: string
9+
contextWindow: string
10+
maxOutput: string
11+
pricing: string
12+
input: string
13+
output: string
14+
cache: string
15+
}
16+
}
17+
18+
/**
19+
* ModelSpecifications Section
20+
*
21+
* Displays technical specifications for AI models including size,
22+
* context window, max output, and token pricing.
23+
*/
24+
export function ModelSpecifications({ model, translations }: ModelSpecificationsProps) {
25+
const hasContent =
26+
model.size ||
27+
model.contextWindow ||
28+
model.maxOutput ||
29+
model.tokenPricing?.input !== null ||
30+
model.tokenPricing?.output !== null ||
31+
model.tokenPricing?.cache !== null
32+
33+
if (!hasContent) {
34+
return null
35+
}
36+
37+
return (
38+
<section className="py-[var(--spacing-lg)] border-b border-[var(--color-border)]">
39+
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
40+
<h2 className="text-2xl font-semibold tracking-[-0.02em] mb-[var(--spacing-sm)]">
41+
{translations.title}
42+
</h2>
43+
44+
<div className="grid grid-cols-1 md:grid-cols-2 gap-[var(--spacing-md)] mt-[var(--spacing-lg)]">
45+
{model.size && (
46+
<div className="border border-[var(--color-border)] p-[var(--spacing-md)]">
47+
<h3 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium mb-[var(--spacing-xs)]">
48+
{translations.modelSize}
49+
</h3>
50+
<p className="text-lg font-semibold tracking-tight">{model.size}</p>
51+
</div>
52+
)}
53+
54+
<div className="border border-[var(--color-border)] p-[var(--spacing-md)]">
55+
<h3 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium mb-[var(--spacing-xs)]">
56+
{translations.contextWindow}
57+
</h3>
58+
<p className="text-lg font-semibold tracking-tight">
59+
{formatTokenCount(model.contextWindow)}
60+
</p>
61+
</div>
62+
63+
<div className="border border-[var(--color-border)] p-[var(--spacing-md)]">
64+
<h3 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium mb-[var(--spacing-xs)]">
65+
{translations.maxOutput}
66+
</h3>
67+
<p className="text-lg font-semibold tracking-tight">
68+
{formatTokenCount(model.maxOutput)}
69+
</p>
70+
</div>
71+
72+
{model.tokenPricing && (
73+
<div className="border border-[var(--color-border)] p-[var(--spacing-md)]">
74+
<h3 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium mb-[var(--spacing-xs)]">
75+
{translations.pricing}
76+
</h3>
77+
<div className="space-y-1">
78+
{model.tokenPricing.input !== null && model.tokenPricing.input !== undefined && (
79+
<p className="text-sm">
80+
<span className="text-[var(--color-text-muted)] text-xs">
81+
{translations.input}{' '}
82+
</span>
83+
<span className="font-semibold tracking-tight">
84+
${model.tokenPricing.input}/M
85+
</span>
86+
</p>
87+
)}
88+
{model.tokenPricing.output !== null && model.tokenPricing.output !== undefined && (
89+
<p className="text-sm">
90+
<span className="text-[var(--color-text-muted)] text-xs">
91+
{translations.output}{' '}
92+
</span>
93+
<span className="font-semibold tracking-tight">
94+
${model.tokenPricing.output}/M
95+
</span>
96+
</p>
97+
)}
98+
{model.tokenPricing.cache !== null && model.tokenPricing.cache !== undefined && (
99+
<p className="text-sm">
100+
<span className="text-[var(--color-text-muted)] text-xs">
101+
{translations.cache}{' '}
102+
</span>
103+
<span className="font-semibold tracking-tight">
104+
${model.tokenPricing.cache}/M
105+
</span>
106+
</p>
107+
)}
108+
</div>
109+
</div>
110+
)}
111+
</div>
112+
</div>
113+
</section>
114+
)
115+
}

src/components/PlatformIcons.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { SVGProps } from 'react'
2+
3+
export function AppleIcon(props: SVGProps<SVGSVGElement>) {
4+
return (
5+
<svg
6+
xmlns="http://www.w3.org/2000/svg"
7+
width="1em"
8+
height="1em"
9+
viewBox="0 0 24 24"
10+
fill="currentColor"
11+
role="img"
12+
{...props}
13+
>
14+
<title>Apple</title>
15+
{/* Apple logo - exact copy from cursor.com/download */}
16+
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
17+
</svg>
18+
)
19+
}
20+
21+
export function WindowsIcon(props: SVGProps<SVGSVGElement>) {
22+
return (
23+
<svg
24+
xmlns="http://www.w3.org/2000/svg"
25+
width="1.2em"
26+
height="1.2em"
27+
viewBox="0 0 17 17"
28+
fill="currentColor"
29+
style={{ paddingTop: '0.1em' }}
30+
role="img"
31+
{...props}
32+
>
33+
<title>Windows</title>
34+
{/* Windows logo - exact copy from cursor.com/download */}
35+
<path d="M8.32372 2.32812H14.0117V8.01612H8.32372V2.32812ZM2.01172 2.32812H7.69972V8.01612H2.01172V2.32812ZM7.69972 8.64079H2.01172V14.3281H7.69972V8.64079ZM8.32372 8.64079H14.0117V14.3281H8.32372V8.64079Z" />
36+
</svg>
37+
)
38+
}
39+
40+
export function LinuxIcon(props: SVGProps<SVGSVGElement>) {
41+
return (
42+
<svg
43+
xmlns="http://www.w3.org/2000/svg"
44+
width="1.1em"
45+
height="1.1em"
46+
viewBox="0 0 24 24"
47+
fill="currentColor"
48+
style={{ paddingTop: '0.1em' }}
49+
role="img"
50+
{...props}
51+
>
52+
<title>Linux</title>
53+
{/* Linux Tux penguin - exact copy from cursor.com/download */}
54+
<path d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z" />
55+
</svg>
56+
)
57+
}

src/components/PlatformLinks.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { LinkCardGrid } from '@/components/product/LinkCard'
2+
3+
export interface PlatformUrls {
4+
huggingface?: string | null
5+
artificialAnalysis?: string | null
6+
openrouter?: string | null
7+
[key: string]: string | null | undefined
8+
}
9+
10+
export interface PlatformLinksProps {
11+
platformUrls: PlatformUrls | null | undefined
12+
title: string
13+
links: Array<{
14+
key: string
15+
title: string
16+
description: string
17+
}>
18+
layout?: 'horizontal' | 'vertical'
19+
gridCols?: string
20+
}
21+
22+
/**
23+
* PlatformLinks Section
24+
*
25+
* Displays AI platform links for models and providers
26+
* (HuggingFace, Artificial Analysis, OpenRouter).
27+
* Reuses LinkCardGrid for consistent styling.
28+
*/
29+
export function PlatformLinks({
30+
platformUrls,
31+
title,
32+
links,
33+
layout = 'horizontal',
34+
gridCols = 'grid-cols-1 md:grid-cols-3',
35+
}: PlatformLinksProps) {
36+
if (!platformUrls) {
37+
return null
38+
}
39+
40+
return (
41+
<LinkCardGrid
42+
title={title}
43+
links={links}
44+
urls={platformUrls}
45+
layout={layout}
46+
gridCols={gridCols}
47+
/>
48+
)
49+
}

src/components/VendorModels.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Link } from '@/i18n/navigation'
2+
import { formatTokenCount } from '@/lib/format'
3+
import type { ManifestModel } from '@/types/manifests'
4+
5+
export type VendorModelsProps = {
6+
models: ManifestModel[]
7+
locale: string
8+
title: string
9+
}
10+
11+
export function VendorModels({ models, locale: _locale, title }: VendorModelsProps) {
12+
if (models.length === 0) {
13+
return null
14+
}
15+
16+
return (
17+
<section className="max-w-8xl mx-auto px-[var(--spacing-md)] py-[var(--spacing-lg)]">
18+
<h2 className="text-xl font-semibold tracking-tight mb-[var(--spacing-md)]">{title}</h2>
19+
20+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--spacing-md)]">
21+
{models.map(model => (
22+
<Link
23+
key={model.id}
24+
href={`/models/${model.id}`}
25+
className="block border border-[var(--color-border)] p-[var(--spacing-md)] hover:border-[var(--color-border-strong)] transition-all hover:-translate-y-0.5 group"
26+
>
27+
<div className="flex items-start justify-between mb-[var(--spacing-xs)]">
28+
<div className="flex-1">
29+
<h3 className="text-base font-semibold tracking-tight mb-[var(--spacing-xs)]">
30+
{model.name}
31+
</h3>
32+
<p className="text-sm text-[var(--color-text-secondary)] font-light line-clamp-2 mb-[var(--spacing-sm)]">
33+
{model.description}
34+
</p>
35+
36+
{/* Model specs */}
37+
<div className="flex flex-wrap gap-[var(--spacing-xs)] text-xs">
38+
{model.size && (
39+
<span className="px-2 py-0.5 bg-[var(--color-background-muted)] text-[var(--color-text-secondary)] border border-[var(--color-border)]">
40+
{model.size}
41+
</span>
42+
)}
43+
<span className="px-2 py-0.5 bg-[var(--color-background-muted)] text-[var(--color-text-secondary)] border border-[var(--color-border)]">
44+
{formatTokenCount(model.contextWindow)} context
45+
</span>
46+
<span className="px-2 py-0.5 bg-[var(--color-background-muted)] text-[var(--color-text-secondary)] border border-[var(--color-border)]">
47+
{formatTokenCount(model.maxOutput)} output
48+
</span>
49+
</div>
50+
</div>
51+
<span className="text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all ml-[var(--spacing-xs)]">
52+
53+
</span>
54+
</div>
55+
</Link>
56+
))}
57+
</div>
58+
</section>
59+
)
60+
}

0 commit comments

Comments
 (0)