11'use client'
22
3- import Link from 'next/link'
43import { useTranslations } from 'next-intl'
54import { useMemo , useState } from 'react'
65import FilterSortBar from '@/components/controls/FilterSortBar'
76import Footer from '@/components/Footer'
87import Header from '@/components/Header'
9- import StackSidebar from '@/components/sidebar/StackSidebar '
8+ import StackTabs from '@/components/navigation/StackTabs '
109import type { Locale } from '@/i18n/config'
10+ import { Link } from '@/i18n/navigation'
1111import { clisData } from '@/lib/generated'
1212import { translateLicenseText } from '@/lib/license'
1313import { localizeManifestItems } from '@/lib/manifest-i18n'
@@ -22,6 +22,7 @@ export default function CLIsPageClient({ locale }: Props) {
2222 const [ sortOrder , setSortOrder ] = useState < 'default' | 'name-asc' | 'name-desc' > ( 'default' )
2323 const [ licenseFilters , setLicenseFilters ] = useState < string [ ] > ( [ ] )
2424 const [ platformFilters , setPlatformFilters ] = useState < string [ ] > ( [ ] )
25+ const [ searchQuery , setSearchQuery ] = useState ( '' )
2526
2627 // Localize CLIs
2728 const localizedClis = useMemo ( ( ) => {
@@ -35,6 +36,27 @@ export default function CLIsPageClient({ locale }: Props) {
3536 const filteredAndSortedClis = useMemo ( ( ) => {
3637 let result = [ ...localizedClis ]
3738
39+ // Apply search filter (search in name and i18n fields)
40+ if ( searchQuery . trim ( ) ) {
41+ const query = searchQuery . toLowerCase ( )
42+ result = result . filter ( cli => {
43+ // Search in main name
44+ if ( cli . name . toLowerCase ( ) . includes ( query ) ) return true
45+ // Search in i18n names if available
46+ if ( cli . i18n ) {
47+ return Object . values ( cli . i18n ) . some (
48+ translation =>
49+ typeof translation === 'object' &&
50+ translation !== null &&
51+ 'name' in translation &&
52+ typeof translation . name === 'string' &&
53+ translation . name . toLowerCase ( ) . includes ( query )
54+ )
55+ }
56+ return false
57+ } )
58+ }
59+
3860 // Apply license filter
3961 if ( licenseFilters . length > 0 ) {
4062 result = result . filter ( cli => {
@@ -67,79 +89,79 @@ export default function CLIsPageClient({ locale }: Props) {
6789 // 'default' keeps the original order
6890
6991 return result
70- } , [ localizedClis , sortOrder , licenseFilters , platformFilters ] )
92+ } , [ localizedClis , sortOrder , licenseFilters , platformFilters , searchQuery ] )
7193
7294 return (
7395 < >
7496 < Header />
7597
7698 < div className = "max-w-[1400px] mx-auto px-[var(--spacing-md)] py-[var(--spacing-lg)]" >
77- < div className = "flex gap-[var(--spacing-lg)]" >
78- < StackSidebar activeStack = "clis" locale = { locale } />
79-
80- { /* Main Content */ }
81- < main className = "flex-1" >
82- < div className = "mb-[var(--spacing-lg)]" >
83- < div className = "flex items-start justify-between mb-[var(--spacing-sm)]" >
84- < h1 className = "text-[2rem] font-semibold tracking-[-0.03em]" >
85- < span className = "text-[var(--color-text-muted)] font-light mr-[var(--spacing-xs)]" >
86- { '//' }
87- </ span >
88- { t ( 'title' ) }
89- </ h1 >
99+ { /* Main Content */ }
100+ < main className = "w-full" >
101+ < div className = "mb-[var(--spacing-lg)]" >
102+ < div className = "flex items-start justify-between mb-[var(--spacing-sm)]" >
103+ < h1 className = "text-[2rem] font-semibold tracking-[-0.03em]" >
104+ < span className = "text-[var(--color-text-muted)] font-light mr-[var(--spacing-xs)]" >
105+ { '//' }
106+ </ span >
107+ { t ( 'title' ) }
108+ </ h1 >
109+ < Link
110+ href = { `/${ locale } /clis/comparison` }
111+ className = "text-sm px-[var(--spacing-md)] py-[var(--spacing-xs)] border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-colors"
112+ >
113+ { t ( 'compareAll' ) } →
114+ </ Link >
115+ </ div >
116+ < p className = "text-base text-[var(--color-text-secondary)] font-light" >
117+ { t ( 'subtitle' ) }
118+ </ p >
119+ </ div >
120+
121+ < StackTabs activeStack = "clis" locale = { locale } />
122+
123+ < FilterSortBar
124+ sortOrder = { sortOrder }
125+ onSortChange = { setSortOrder }
126+ licenseFilters = { licenseFilters }
127+ onLicenseFiltersChange = { setLicenseFilters }
128+ platformFilters = { platformFilters }
129+ onPlatformFiltersChange = { setPlatformFilters }
130+ searchQuery = { searchQuery }
131+ onSearchChange = { setSearchQuery }
132+ />
133+
134+ { filteredAndSortedClis . length === 0 ? (
135+ < div className = "text-center py-[var(--spacing-xl)] text-[var(--color-text-secondary)]" >
136+ { t ( 'noMatches' ) }
137+ </ div >
138+ ) : (
139+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-[var(--spacing-md)]" >
140+ { filteredAndSortedClis . map ( cli => (
90141 < Link
91- href = { `/${ locale } /clis/comparison` }
92- className = "text-sm px-[var(--spacing-md)] py-[var(--spacing-xs)] border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-colors"
142+ key = { cli . name }
143+ href = { `/${ locale } /clis/${ cli . id } ` }
144+ 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 flex flex-col"
93145 >
94- { t ( 'compareAll' ) } →
146+ < div className = "flex justify-between items-start mb-[var(--spacing-sm)]" >
147+ < h3 className = "text-lg font-semibold tracking-tight" > { cli . name } </ h3 >
148+ < span className = "text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all" >
149+ →
150+ </ span >
151+ </ div >
152+ < p className = "text-sm leading-relaxed text-[var(--color-text-secondary)] mb-[var(--spacing-md)] font-light min-h-[4rem]" >
153+ { cli . description }
154+ </ p >
155+ < div className = "flex items-center gap-[var(--spacing-xs)] text-xs text-[var(--color-text-muted)] mt-auto" >
156+ < span > { cli . vendor } </ span >
157+ < span className = "text-[var(--color-border)]" > •</ span >
158+ < span > { translateLicenseText ( cli . license , tGlobal ) } </ span >
159+ </ div >
95160 </ Link >
96- </ div >
97- < p className = "text-base text-[var(--color-text-secondary)] font-light" >
98- { t ( 'subtitle' ) }
99- </ p >
161+ ) ) }
100162 </ div >
101-
102- < FilterSortBar
103- sortOrder = { sortOrder }
104- onSortChange = { setSortOrder }
105- licenseFilters = { licenseFilters }
106- onLicenseFiltersChange = { setLicenseFilters }
107- platformFilters = { platformFilters }
108- onPlatformFiltersChange = { setPlatformFilters }
109- />
110-
111- { filteredAndSortedClis . length === 0 ? (
112- < div className = "text-center py-[var(--spacing-xl)] text-[var(--color-text-secondary)]" >
113- { t ( 'noMatches' ) }
114- </ div >
115- ) : (
116- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--spacing-md)]" >
117- { filteredAndSortedClis . map ( cli => (
118- < Link
119- key = { cli . name }
120- href = { `/${ locale } /clis/${ cli . id } ` }
121- 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 flex flex-col"
122- >
123- < div className = "flex justify-between items-start mb-[var(--spacing-sm)]" >
124- < h3 className = "text-lg font-semibold tracking-tight" > { cli . name } </ h3 >
125- < span className = "text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all" >
126- →
127- </ span >
128- </ div >
129- < p className = "text-sm leading-relaxed text-[var(--color-text-secondary)] mb-[var(--spacing-md)] font-light min-h-[4rem]" >
130- { cli . description }
131- </ p >
132- < div className = "flex items-center gap-[var(--spacing-xs)] text-xs text-[var(--color-text-muted)] mt-auto" >
133- < span > { cli . vendor } </ span >
134- < span className = "text-[var(--color-border)]" > •</ span >
135- < span > { translateLicenseText ( cli . license , tGlobal ) } </ span >
136- </ div >
137- </ Link >
138- ) ) }
139- </ div >
140- ) }
141- </ main >
142- </ div >
163+ ) }
164+ </ main >
143165 </ div >
144166
145167 < Footer />
0 commit comments