Skip to content

Commit 3e8695a

Browse files
ericyangpanclaude
andcommitted
refactor(components): update UI components
Update all components to support new manifest structure, improve type safety, and enhance functionality for product displays, navigation, controls, and sidebars. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 8613838 commit 3e8695a

27 files changed

+245
-191
lines changed

src/components/Footer.tsx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,42 +35,40 @@ function FooterLinkList({ title, links }: FooterLinkListProps) {
3535
}
3636

3737
export default function Footer() {
38-
const tFooter = useTranslations('components.footer')
39-
const tPlatforms = useTranslations('shared.platforms')
40-
const tStacks = useTranslations('shared.categories.plural')
41-
const tTerms = useTranslations('shared.terms')
38+
const tComponents = useTranslations('components')
39+
const tShared = useTranslations('shared')
4240

4341
// Define link arrays (static hrefs, only labels depend on translations)
4442
const resourceLinks = [
45-
{ href: '/ides', label: tStacks('ides') },
46-
{ href: '/clis', label: tStacks('clis') },
47-
{ href: '/extensions', label: tStacks('extensions') },
48-
{ href: '/models', label: tStacks('models') },
49-
{ href: '/model-providers', label: tStacks('modelProviders') },
50-
{ href: '/vendors', label: tStacks('vendors') },
43+
{ href: '/ides', label: tShared('categories.plural.ides') },
44+
{ href: '/clis', label: tShared('categories.plural.clis') },
45+
{ href: '/extensions', label: tShared('categories.plural.extensions') },
46+
{ href: '/models', label: tShared('categories.plural.models') },
47+
{ href: '/model-providers', label: tShared('categories.plural.modelProviders') },
48+
{ href: '/vendors', label: tShared('categories.plural.vendors') },
5149
]
5250

5351
const documentationLinks = [
54-
{ href: '/docs', label: tTerms('docs') },
55-
{ href: '/articles', label: tTerms('articles') },
56-
{ href: '/curated-collections', label: tTerms('curatedCollections') },
57-
{ href: '/#faq', label: tTerms('faq') },
52+
{ href: '/docs', label: tShared('terms.docs') },
53+
{ href: '/articles', label: tShared('terms.articles') },
54+
{ href: '/curated-collections', label: tShared('terms.curatedCollections') },
55+
{ href: '/#faq', label: tShared('terms.faq') },
5856
]
5957

6058
const communityLinks = [
6159
{
6260
href: 'https://github.com/aicodingstack/aicodingstack.io',
63-
label: tPlatforms('github'),
61+
label: tShared('platforms.github'),
6462
isExternal: true,
6563
},
6664
{
6765
href: 'https://aicodingstack.io/discord',
68-
label: tPlatforms('discord'),
66+
label: tShared('platforms.discord'),
6967
isExternal: true,
7068
},
7169
{
7270
href: 'https://x.com/aicodingstack',
73-
label: tPlatforms('twitter'),
71+
label: tShared('platforms.twitter'),
7472
isExternal: false,
7573
},
7674
]
@@ -79,24 +77,26 @@ export default function Footer() {
7977
<footer className="bg-[var(--color-bg)] max-w-8xl mx-auto px-[var(--spacing-md)] mt-[var(--spacing-lg)]">
8078
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-9 gap-[var(--spacing-lg)] py-[var(--spacing-lg)] border-y border-[var(--color-border)]">
8179
<div className="flex flex-col gap-[var(--spacing-sm)] lg:col-span-3">
82-
<span className="text-sm font-semibold tracking-tight">{tTerms('aiCodingStack')}</span>
80+
<span className="text-sm font-semibold tracking-tight">
81+
{tShared('terms.aiCodingStack')}
82+
</span>
8383
<p className="text-sm pb-[var(--spacing-sm)] leading-[1.8] text-[var(--color-text-secondary)] font-light">
84-
{tFooter('tagline')}
85-
<span className="block mt-[var(--spacing-sm)]">{tFooter('openSource')}</span>
84+
{tComponents('footer.tagline')}
85+
<span className="block mt-[var(--spacing-sm)]">{tComponents('footer.openSource')}</span>
8686
</p>
8787
<div className="flex gap-[var(--spacing-xs)]">
8888
<ThemeSwitcher />
8989
<LanguageSwitcher />
9090
</div>
9191
</div>
9292

93-
<FooterLinkList title={tTerms('resources')} links={resourceLinks} />
94-
<FooterLinkList title={tTerms('documentation')} links={documentationLinks} />
95-
<FooterLinkList title={tTerms('community')} links={communityLinks} />
93+
<FooterLinkList title={tShared('terms.resources')} links={resourceLinks} />
94+
<FooterLinkList title={tShared('terms.documentation')} links={documentationLinks} />
95+
<FooterLinkList title={tShared('terms.community')} links={communityLinks} />
9696
</div>
9797

9898
<div className="py-[var(--spacing-md)] text-center text-xs text-[var(--color-text-muted)]">
99-
{tFooter('copyright')}
99+
{tComponents('footer.copyright')}
100100
</div>
101101
</footer>
102102
)

src/components/Header.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function Header() {
3232
const [isMenuOpen, setIsMenuOpen] = useState(false)
3333
const [activeMegaMenu, setActiveMegaMenu] = useState<'aiCodingStack' | 'ranking' | null>(null)
3434
const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false)
35-
const t = useTranslations('components.header')
35+
const tComponents = useTranslations('components')
3636

3737
// Menu items configuration - memoized to avoid recreation on each render
3838
const menuItems = useMemo<MenuItem[]>(
@@ -107,7 +107,7 @@ function Header() {
107107
aria-expanded={isActive}
108108
aria-haspopup="true"
109109
>
110-
{t(item.translationKey as never)}
110+
{tComponents(`header.${item.translationKey}`)}
111111
</Link>
112112
{item.megaMenuType === 'aiCodingStack' && (
113113
<StackMegaMenu isOpen={isActive} onClose={handleMegaMenuClose} />
@@ -123,17 +123,17 @@ function Header() {
123123
<li key={item.href}>
124124
{item.isExternal ? (
125125
<a href={item.href} target="_blank" rel="noopener" className={DESKTOP_LINK_CLASSES}>
126-
{t(item.translationKey as never)}
126+
{tComponents(`header.${item.translationKey}`)}
127127
</a>
128128
) : (
129129
<Link href={item.href} className={DESKTOP_LINK_CLASSES}>
130-
{t(item.translationKey as never)}
130+
{tComponents(`header.${item.translationKey}`)}
131131
</Link>
132132
)}
133133
</li>
134134
)
135135
},
136-
[activeMegaMenu, handleMegaMenuOpen, handleMegaMenuClose, t]
136+
[activeMegaMenu, handleMegaMenuOpen, handleMegaMenuClose, tComponents]
137137
)
138138

139139
// Render mobile menu item
@@ -142,22 +142,22 @@ function Header() {
142142
<li key={item.href}>
143143
{item.isExternal ? (
144144
<a href={item.href} target="_blank" rel="noopener" className={MOBILE_LINK_CLASSES}>
145-
{t(item.translationKey as never)}
145+
{tComponents(`header.${item.translationKey}`)}
146146
</a>
147147
) : (
148148
<Link href={item.href} className={MOBILE_LINK_CLASSES} onClick={handleMenuClose}>
149-
{t(item.translationKey as never)}
149+
{tComponents(`header.${item.translationKey}`)}
150150
</Link>
151151
)}
152152
</li>
153153
),
154-
[handleMenuClose, t]
154+
[handleMenuClose, tComponents]
155155
)
156156

157157
// Memoized menu button label
158158
const menuButtonLabel = useMemo(
159-
() => (isMenuOpen ? t('closeMenu') : t('openMenu')),
160-
[isMenuOpen, t]
159+
() => (isMenuOpen ? tComponents('header.closeMenu') : tComponents('header.openMenu')),
160+
[isMenuOpen, tComponents]
161161
)
162162

163163
return (
@@ -206,7 +206,7 @@ function Header() {
206206
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
207207
/>
208208
</svg>
209-
<span className="flex-1 text-left">{t('searchPlaceholder')}</span>
209+
<span className="flex-1 text-left">{tComponents('header.searchPlaceholder')}</span>
210210
<kbd className="flex items-center gap-1 px-1.5 py-0.5 text-xs border border-[var(--color-border)]">
211211
<Command className="w-3 h-3" />
212212
<span>K</span>
@@ -221,7 +221,7 @@ function Header() {
221221
type="button"
222222
onClick={() => setIsSearchDialogOpen(true)}
223223
className="p-[var(--spacing-xs)] hover:bg-[var(--color-hover)] transition-colors"
224-
aria-label={t('search')}
224+
aria-label={tComponents('header.search')}
225225
>
226226
<svg
227227
className="w-5 h-5"
@@ -244,7 +244,7 @@ function Header() {
244244
type="button"
245245
onClick={handleMenuToggle}
246246
className="p-[var(--spacing-xs)] hover:bg-[var(--color-hover)] transition-colors"
247-
aria-label={t('toggleMenu')}
247+
aria-label={tComponents('header.toggleMenu')}
248248
>
249249
<svg
250250
className="w-6 h-6"

src/components/controls/CopyButton.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
'use client'
22

3+
import { useTranslations } from 'next-intl'
34
import { useState } from 'react'
45

56
export default function CopyButton({ text }: { text: string }) {
7+
const tComponents = useTranslations('components')
68
const [copied, setCopied] = useState(false)
79

810
const handleCopy = async () => {
@@ -24,8 +26,12 @@ export default function CopyButton({ text }: { text: string }) {
2426
? 'text-green-600'
2527
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
2628
}`}
27-
title={copied ? 'Copied!' : 'Copy to clipboard'}
28-
aria-label={copied ? 'Copied!' : 'Copy to clipboard'}
29+
title={
30+
copied ? tComponents('copyButton.copied') : tComponents('copyButton.copyToClipboard')
31+
}
32+
aria-label={
33+
copied ? tComponents('copyButton.copied') : tComponents('copyButton.copyToClipboard')
34+
}
2935
>
3036
{copied ? (
3137
<svg
@@ -39,7 +45,7 @@ export default function CopyButton({ text }: { text: string }) {
3945
strokeLinejoin="round"
4046
role="img"
4147
>
42-
<title>Copied</title>
48+
<title>{tComponents('copyButton.copied')}</title>
4349
<polyline points="20 6 9 17 4 12"></polyline>
4450
</svg>
4551
) : (
@@ -54,7 +60,7 @@ export default function CopyButton({ text }: { text: string }) {
5460
strokeLinejoin="round"
5561
role="img"
5662
>
57-
<title>Copy to clipboard</title>
63+
<title>{tComponents('copyButton.copyToClipboard')}</title>
5864
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
5965
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
6066
</svg>

src/components/controls/FilterSortBar.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,19 @@ export default function FilterSortBar({
2828
searchQuery = '',
2929
onSearchChange,
3030
}: FilterSortBarProps) {
31-
const t = useTranslations('components.filterSortBar')
31+
const tComponents = useTranslations('components')
3232
const [isSortOpen, setIsSortOpen] = useState(false)
3333
const sortRef = useRef<HTMLDivElement>(null)
3434

3535
const sortOptions = [
36-
{ value: 'default', label: t('sortDefault') },
37-
{ value: 'name-asc', label: t('sortNameAsc') },
38-
{ value: 'name-desc', label: t('sortNameDesc') },
36+
{ value: 'default', label: tComponents('filterSortBar.sortDefault') },
37+
{ value: 'name-asc', label: tComponents('filterSortBar.sortNameAsc') },
38+
{ value: 'name-desc', label: tComponents('filterSortBar.sortNameDesc') },
3939
]
4040

4141
const currentSortLabel =
42-
sortOptions.find(opt => opt.value === sortOrder)?.label || t('sortDefault')
42+
sortOptions.find(opt => opt.value === sortOrder)?.label ||
43+
tComponents('filterSortBar.sortDefault')
4344

4445
// Close dropdown when clicking outside
4546
useEffect(() => {
@@ -90,15 +91,17 @@ export default function FilterSortBar({
9091
type="text"
9192
value={searchQuery}
9293
onChange={e => onSearchChange(e.target.value)}
93-
placeholder={t('search')}
94+
placeholder={tComponents('filterSortBar.search')}
9495
className="flex-1 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"
9596
/>
9697
</div>
9798
)}
9899

99100
{/* Sort Custom Dropdown */}
100101
<div className="flex items-center gap-[var(--spacing-xs)]">
101-
<span className="text-xs text-[var(--color-text-muted)]">{t('sort')}</span>
102+
<span className="text-xs text-[var(--color-text-muted)]">
103+
{tComponents('filterSortBar.sort')}
104+
</span>
102105
<div className="relative" ref={sortRef}>
103106
<button
104107
type="button"
@@ -135,7 +138,9 @@ export default function FilterSortBar({
135138

136139
{/* License Filter Buttons */}
137140
<div className="flex items-center gap-[var(--spacing-xs)]">
138-
<span className="text-xs text-[var(--color-text-muted)]">{t('license')}</span>
141+
<span className="text-xs text-[var(--color-text-muted)]">
142+
{tComponents('filterSortBar.license')}
143+
</span>
139144
<button
140145
type="button"
141146
onClick={() => toggleLicense('open-source')}
@@ -145,7 +150,7 @@ export default function FilterSortBar({
145150
: 'border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)]'
146151
}`}
147152
>
148-
{t('openSource')}
153+
{tComponents('filterSortBar.openSource')}
149154
</button>
150155
<button
151156
type="button"
@@ -156,14 +161,14 @@ export default function FilterSortBar({
156161
: 'border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)]'
157162
}`}
158163
>
159-
{t('proprietary')}
164+
{tComponents('filterSortBar.proprietary')}
160165
</button>
161166
</div>
162167

163168
{/* Platform Filter Buttons */}
164169
<div className="flex items-center gap-[var(--spacing-xs)]">
165170
<span className="text-xs text-[var(--color-text-muted)]">
166-
{platformLabel || t('platform')}
171+
{platformLabel || tComponents('filterSortBar.platform')}
167172
</span>
168173
{availablePlatforms.map(platform => (
169174
<button
@@ -188,7 +193,7 @@ export default function FilterSortBar({
188193
onClick={clearFilters}
189194
className="ml-auto px-[var(--spacing-sm)] py-1 text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors"
190195
>
191-
{t('clearFilters')}
196+
{tComponents('filterSortBar.clearFilters')}
192197
</button>
193198
)}
194199
</div>

src/components/controls/LanguageSwitcher.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default function LanguageSwitcher() {
5858
const locale = useLocale() as Locale
5959
const router = useRouter()
6060
const pathname = usePathname()
61-
const t = useTranslations('components.footer')
61+
const tComponents = useTranslations('components')
6262
const [isOpen, setIsOpen] = useState(false)
6363
const dropdownRef = useRef<HTMLDivElement>(null)
6464

@@ -120,16 +120,16 @@ export default function LanguageSwitcher() {
120120
type="button"
121121
onClick={() => setIsOpen(!isOpen)}
122122
className="footer-control-button"
123-
title={t('selectLanguage')}
124-
aria-label={t('selectLanguage')}
123+
title={tComponents('footer.selectLanguage')}
124+
aria-label={tComponents('footer.selectLanguage')}
125125
aria-expanded={isOpen}
126126
>
127127
<Languages className="footer-control-icon" />
128128
{localeNames[locale]}
129129
</button>
130130

131131
{isOpen && (
132-
<div className="absolute bottom-full left-0 mb-1 min-w-[120px] bg-[var(--color-bg)] border border-[var(--color-border)] shadow-lg">
132+
<div className="absolute bottom-full left-0 mb-1 min-w-[160px] bg-[var(--color-bg)] border border-[var(--color-border)] shadow-lg">
133133
{locales.map(loc => (
134134
<button
135135
type="button"

src/components/controls/ThemeSwitcher.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import { useTheme } from '../ThemeProvider'
1010
*/
1111
export default function ThemeSwitcher() {
1212
const { theme, toggleTheme } = useTheme()
13-
const t = useTranslations('components.footer')
13+
const tComponents = useTranslations('components')
1414

1515
return (
1616
<button
1717
type="button"
1818
onClick={toggleTheme}
1919
className="footer-control-button"
20-
title={t('toggleTheme')}
21-
aria-label={t('toggleTheme')}
20+
title={tComponents('footer.toggleTheme')}
21+
aria-label={tComponents('footer.toggleTheme')}
2222
>
2323
{theme === 'light' ? (
2424
<Moon className="footer-control-icon" />

src/components/navigation/BackToNavigation.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ interface BackToNavigationProps {
2424
* <BackToNavigation href="/ides" title="All IDEs" />
2525
*/
2626
export function BackToNavigation({ href, title }: BackToNavigationProps) {
27-
const t = useTranslations('components.backToNavigation')
27+
const tComponents = useTranslations('components')
2828

2929
return (
3030
<section className="pt-[var(--spacing-lg)]">
@@ -34,10 +34,12 @@ export function BackToNavigation({ href, title }: BackToNavigationProps) {
3434
className="border border-[var(--color-border)] p-[var(--spacing-md)] hover:border-[var(--color-border-strong)] transition-all hover:-translate-y-0.5 flex flex-col gap-1 text-center"
3535
>
3636
<span className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium">
37-
{t('backTo')}
37+
{tComponents('backToNavigation.backTo')}
3838
</span>
3939
<span className="text-lg font-semibold tracking-tight">{title}</span>
40-
<span className="text-xs text-[var(--color-text-muted)]">[{t('indexLabel')}]</span>
40+
<span className="text-xs text-[var(--color-text-muted)]">
41+
[{tComponents('backToNavigation.indexLabel')}]
42+
</span>
4143
</Link>
4244
</div>
4345
</section>

0 commit comments

Comments
 (0)