Skip to content

Commit 38fe65d

Browse files
ericyangpanclaude
andcommitted
refactor(ui): extract client components and reorganize navigation
- Extract client components: model-providers/page.client.tsx, models/page.client.tsx, vendors/page.client.tsx - Add navigation components directory (src/components/navigation/) - Add vendor components directory (src/components/vendor/) - Refactor Header, Footer, and MegaMenu for better modularity - Update Breadcrumb component with improved navigation - Update FilterSortBar with enhanced search functionality - Update product components (GitHubStarHistory, LinkCard, ProductCommands, ProductHero, ProductLinks, ProductPricing) - Update sidebar and comparison table components - Update layout.tsx and globals.css for better styling consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 12dfce6 commit 38fe65d

24 files changed

+990
-280
lines changed

src/app/[locale]/globals.css

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,6 @@ strong {
100100
animation: fadeIn 200ms ease-out;
101101
}
102102

103-
/* Code syntax highlighting theme switching */
104-
:root {
105-
@layer highlight-light;
106-
}
107-
108-
[data-theme="dark"] {
109-
@layer highlight-dark;
110-
}
111-
112103
/* Code block styling */
113104
pre {
114105
overflow-x: auto;

src/app/[locale]/layout.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import './globals.css'
88
import ClientLayout from '@/components/ClientLayout'
99
import { JsonLd } from '@/components/JsonLd'
1010
import { defaultLocale, type Locale, locales } from '@/i18n/config'
11+
import { SITE_CONFIG } from '@/lib/metadata/config'
1112
import { getLanguageAlternates, getOgAlternateLocales, getOgLocale } from '@/lib/seo-helpers'
1213
import { WebVitals } from './web-vitals'
1314

@@ -50,7 +51,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
5051

5152
// Get canonical path based on locale
5253
const canonicalPath = locale === defaultLocale ? '/' : `/${locale}`
53-
const baseUrl = 'https://aicodingstack.io'
54+
const baseUrl = SITE_CONFIG.url
5455

5556
return {
5657
metadataBase: new URL(baseUrl),
@@ -116,8 +117,8 @@ const organizationSchema = {
116117
'@context': 'https://schema.org',
117118
'@type': 'Organization',
118119
name: 'AI Coding Stack',
119-
url: 'https://aicodingstack.io',
120-
logo: 'https://aicodingstack.io/logo.png',
120+
url: SITE_CONFIG.url,
121+
logo: `${SITE_CONFIG.url}/logo.png`,
121122
description:
122123
'Comprehensive directory and community-maintained metadata repository for AI-powered coding tools, models, and platforms.',
123124
foundingDate: '2025',
@@ -133,12 +134,12 @@ const websiteSchema = {
133134
'@context': 'https://schema.org',
134135
'@type': 'WebSite',
135136
name: 'AI Coding Stack',
136-
url: 'https://aicodingstack.io',
137+
url: SITE_CONFIG.url,
137138
description:
138139
'Comprehensive directory for AI coding tools across IDEs, CLIs, MCP servers, models and providers.',
139140
potentialAction: {
140141
'@type': 'SearchAction',
141-
target: 'https://aicodingstack.io/search?q={search_term_string}',
142+
target: `${SITE_CONFIG.url}/search?q={search_term_string}`,
142143
'query-input': 'required name=search_term_string',
143144
},
144145
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use client'
2+
3+
import { useTranslations } from 'next-intl'
4+
import { useMemo, useState } from 'react'
5+
import Footer from '@/components/Footer'
6+
import Header from '@/components/Header'
7+
import StackTabs from '@/components/navigation/StackTabs'
8+
import type { Locale } from '@/i18n/config'
9+
import { Link } from '@/i18n/navigation'
10+
import { providersData } from '@/lib/generated'
11+
import { localizeManifestItems } from '@/lib/manifest-i18n'
12+
import type { ManifestProvider } from '@/types/manifests'
13+
14+
type Props = {
15+
locale: string
16+
}
17+
18+
export default function ModelProvidersPageClient({ locale }: Props) {
19+
const t = useTranslations('stacksPages.modelProviders')
20+
const [searchQuery, setSearchQuery] = useState('')
21+
22+
// Localize providers
23+
const localizedProviders = useMemo(() => {
24+
return localizeManifestItems(
25+
providersData as unknown as Record<string, unknown>[],
26+
locale as Locale
27+
) as unknown as ManifestProvider[]
28+
}, [locale])
29+
30+
// Filter providers
31+
const filteredProviders = useMemo(() => {
32+
let result = [...localizedProviders]
33+
34+
// Apply search filter (search in name and i18n fields)
35+
if (searchQuery.trim()) {
36+
const query = searchQuery.toLowerCase()
37+
result = result.filter(provider => {
38+
// Search in main name
39+
if (provider.name.toLowerCase().includes(query)) return true
40+
// Search in i18n names if available
41+
if (provider.i18n) {
42+
return Object.values(provider.i18n).some(
43+
translation =>
44+
typeof translation === 'object' &&
45+
translation !== null &&
46+
'name' in translation &&
47+
typeof translation.name === 'string' &&
48+
translation.name.toLowerCase().includes(query)
49+
)
50+
}
51+
return false
52+
})
53+
}
54+
55+
return result
56+
}, [localizedProviders, searchQuery])
57+
58+
const foundationModelProviders = filteredProviders.filter(
59+
p => p.type === 'foundation-model-provider'
60+
)
61+
const modelServiceProviders = filteredProviders.filter(p => p.type === 'model-service-provider')
62+
63+
return (
64+
<>
65+
<Header />
66+
67+
<div className="max-w-[1400px] mx-auto px-[var(--spacing-md)] py-[var(--spacing-lg)]">
68+
{/* Main Content */}
69+
<main className="w-full">
70+
<div className="mb-[var(--spacing-lg)]">
71+
<h1 className="text-[2rem] font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
72+
<span className="text-[var(--color-text-muted)] font-light mr-[var(--spacing-xs)]">
73+
{'//'}
74+
</span>
75+
{t('title')}
76+
</h1>
77+
<p className="text-base text-[var(--color-text-secondary)] font-light">
78+
{t('subtitle')}
79+
</p>
80+
</div>
81+
82+
<StackTabs activeStack="model-providers" locale={locale} />
83+
84+
{/* Search Box */}
85+
<div className="mb-[var(--spacing-md)]">
86+
<input
87+
type="text"
88+
value={searchQuery}
89+
onChange={e => setSearchQuery(e.target.value)}
90+
placeholder={t('search') || 'Search by name...'}
91+
className="w-full max-w-[300px] px-[var(--spacing-sm)] py-1 text-sm border border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-border-strong)] transition-colors"
92+
/>
93+
</div>
94+
95+
<section className="mb-[var(--spacing-lg)]">
96+
<h2 className="text-base uppercase tracking-wide text-[var(--color-text-muted)] mb-[var(--spacing-sm)]">
97+
{t('foundationProviders')}
98+
</h2>
99+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-[var(--spacing-md)]">
100+
{foundationModelProviders.map(provider => (
101+
<Link
102+
key={provider.name}
103+
href={`/${locale}/model-providers/${provider.id}`}
104+
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"
105+
>
106+
<div className="flex justify-between items-start mb-[var(--spacing-sm)]">
107+
<h3 className="text-lg font-semibold tracking-tight">{provider.name}</h3>
108+
<span className="text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all">
109+
110+
</span>
111+
</div>
112+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)] font-light min-h-[4rem]">
113+
{provider.description}
114+
</p>
115+
</Link>
116+
))}
117+
</div>
118+
</section>
119+
120+
<section>
121+
<h2 className="text-base uppercase tracking-wide text-[var(--color-text-muted)] mb-[var(--spacing-sm)]">
122+
{t('serviceProviders')}
123+
</h2>
124+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-[var(--spacing-md)]">
125+
{modelServiceProviders.map(provider => (
126+
<Link
127+
key={provider.name}
128+
href={`/${locale}/model-providers/${provider.id}`}
129+
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"
130+
>
131+
<div className="flex justify-between items-start mb-[var(--spacing-sm)]">
132+
<h3 className="text-lg font-semibold tracking-tight">{provider.name}</h3>
133+
<span className="text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all">
134+
135+
</span>
136+
</div>
137+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)] font-light min-h-[4rem]">
138+
{provider.description}
139+
</p>
140+
</Link>
141+
))}
142+
</div>
143+
</section>
144+
</main>
145+
</div>
146+
147+
<Footer />
148+
</>
149+
)
150+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use client'
2+
3+
import { useTranslations } from 'next-intl'
4+
import { useMemo, useState } from 'react'
5+
import Footer from '@/components/Footer'
6+
import Header from '@/components/Header'
7+
import StackTabs from '@/components/navigation/StackTabs'
8+
import type { Locale } from '@/i18n/config'
9+
import { Link } from '@/i18n/navigation'
10+
import { modelsData } from '@/lib/generated'
11+
import { localizeManifestItems } from '@/lib/manifest-i18n'
12+
import type { ManifestModel } from '@/types/manifests'
13+
14+
type Props = {
15+
locale: string
16+
}
17+
18+
export default function ModelsPageClient({ locale }: Props) {
19+
const t = useTranslations('stacksPages.models')
20+
const [searchQuery, setSearchQuery] = useState('')
21+
22+
// Localize models
23+
const localizedModels = useMemo(() => {
24+
return localizeManifestItems(
25+
modelsData as unknown as Record<string, unknown>[],
26+
locale as Locale
27+
) as unknown as ManifestModel[]
28+
}, [locale])
29+
30+
// Filter models
31+
const filteredModels = useMemo(() => {
32+
let result = [...localizedModels]
33+
34+
// Apply search filter (search in name and i18n fields)
35+
if (searchQuery.trim()) {
36+
const query = searchQuery.toLowerCase()
37+
result = result.filter(model => {
38+
// Search in main name
39+
if (model.name.toLowerCase().includes(query)) return true
40+
// Search in i18n names if available
41+
if (model.i18n) {
42+
return Object.values(model.i18n).some(
43+
translation =>
44+
typeof translation === 'object' &&
45+
translation !== null &&
46+
'name' in translation &&
47+
typeof translation.name === 'string' &&
48+
translation.name.toLowerCase().includes(query)
49+
)
50+
}
51+
return false
52+
})
53+
}
54+
55+
return result
56+
}, [localizedModels, searchQuery])
57+
58+
return (
59+
<>
60+
<Header />
61+
62+
<div className="max-w-[1400px] mx-auto px-[var(--spacing-md)] py-[var(--spacing-lg)]">
63+
{/* Main Content */}
64+
<main className="w-full">
65+
<div className="mb-[var(--spacing-lg)]">
66+
<div className="flex items-start justify-between mb-[var(--spacing-sm)]">
67+
<h1 className="text-[2rem] font-semibold tracking-[-0.03em]">
68+
<span className="text-[var(--color-text-muted)] font-light mr-[var(--spacing-xs)]">
69+
{'//'}
70+
</span>
71+
{t('title')}
72+
</h1>
73+
<Link
74+
href={`/${locale}/models/comparison`}
75+
className="text-sm px-[var(--spacing-md)] py-[var(--spacing-xs)] border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-colors"
76+
>
77+
{t('compareAll')}
78+
</Link>
79+
</div>
80+
<p className="text-base text-[var(--color-text-secondary)] font-light">
81+
{t('subtitle')}
82+
</p>
83+
</div>
84+
85+
<StackTabs activeStack="models" locale={locale} />
86+
87+
{/* Search Box */}
88+
<div className="mb-[var(--spacing-md)]">
89+
<input
90+
type="text"
91+
value={searchQuery}
92+
onChange={e => setSearchQuery(e.target.value)}
93+
placeholder={t('search') || 'Search by name...'}
94+
className="w-full max-w-[300px] px-[var(--spacing-sm)] py-1 text-sm border border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-border-strong)] transition-colors"
95+
/>
96+
</div>
97+
98+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-[var(--spacing-md)]">
99+
{filteredModels.map(model => (
100+
<Link
101+
key={model.name}
102+
href={`/${locale}/models/${model.id}`}
103+
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"
104+
>
105+
<div className="flex justify-between items-start mb-[var(--spacing-sm)]">
106+
<h3 className="text-lg font-semibold tracking-tight">{model.name}</h3>
107+
<span className="text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all">
108+
109+
</span>
110+
</div>
111+
<div className="space-y-[var(--spacing-xs)] mb-[var(--spacing-md)]">
112+
<div className="flex items-center gap-[var(--spacing-sm)] text-xs">
113+
<span className="text-[var(--color-text-muted)]">{t('size')}</span>
114+
<span className="text-[var(--color-text-secondary)]">{model.size}</span>
115+
</div>
116+
<div className="flex items-center gap-[var(--spacing-sm)] text-xs">
117+
<span className="text-[var(--color-text-muted)]">{t('context')}</span>
118+
<span className="text-[var(--color-text-secondary)]">{model.totalContext}</span>
119+
</div>
120+
<div className="flex items-center gap-[var(--spacing-sm)] text-xs">
121+
<span className="text-[var(--color-text-muted)]">{t('pricing')}</span>
122+
<span className="text-[var(--color-text-secondary)]">
123+
{model.tokenPricing?.input !== null && model.tokenPricing?.input !== undefined
124+
? `$${model.tokenPricing.input}/M`
125+
: '-'}
126+
</span>
127+
</div>
128+
</div>
129+
<div className="flex items-center gap-[var(--spacing-xs)] text-xs text-[var(--color-text-muted)]">
130+
<span>{model.vendor}</span>
131+
</div>
132+
</Link>
133+
))}
134+
</div>
135+
</main>
136+
</div>
137+
138+
<Footer />
139+
</>
140+
)
141+
}

0 commit comments

Comments
 (0)