11'use client' ;
22import React , { useEffect , useState } from 'react' ;
3+ import Navbar from '../components/Navbar' ;
4+ import Footer from '../components/Footer' ;
35
46type Feed = {
57 title : string ;
@@ -15,24 +17,47 @@ type Category = {
1517
1618export default function App ( ) {
1719 const [ data , setData ] = useState < Category [ ] > ( [ ] ) ;
20+ const [ filteredData , setFilteredData ] = useState < Category [ ] > ( [ ] ) ;
1821 const [ selected , setSelected ] = useState < Record < string , boolean > > ( { } ) ;
1922 const [ loading , setLoading ] = useState ( true ) ;
2023 const [ opml , setOpml ] = useState ( '' ) ;
24+ const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
2125
2226 useEffect ( ( ) => {
2327 fetch ( '/feeds.json' )
2428 . then ( res => {
2529 if ( ! res . ok ) throw new Error ( 'feeds.json not found' ) ;
2630 return res . json ( ) ;
2731 } )
28- . then ( ( json : Category [ ] ) => setData ( json ) )
32+ . then ( ( json : Category [ ] ) => {
33+ setData ( json ) ;
34+ setFilteredData ( json ) ;
35+ } )
2936 . catch ( err => {
3037 console . error ( err ) ;
3138 setData ( [ ] ) ;
39+ setFilteredData ( [ ] ) ;
3240 } )
3341 . finally ( ( ) => setLoading ( false ) ) ;
3442 } , [ ] ) ;
3543
44+ // Filter data based on search query
45+ useEffect ( ( ) => {
46+ if ( ! searchQuery . trim ( ) ) {
47+ setFilteredData ( data ) ;
48+ } else {
49+ const filtered = data . map ( category => ( {
50+ ...category ,
51+ feeds : category . feeds . filter ( feed =>
52+ feed . title . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
53+ feed . xmlUrl . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
54+ ( feed . htmlUrl && feed . htmlUrl . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) )
55+ )
56+ } ) ) . filter ( category => category . feeds . length > 0 ) ;
57+ setFilteredData ( filtered ) ;
58+ }
59+ } , [ data , searchQuery ] ) ;
60+
3661 const feedKey = ( c : string , f : Feed ) => `${ c } ||${ f . xmlUrl } ` ;
3762
3863 const toggleFeed = ( c : string , f : Feed ) => {
@@ -106,55 +131,140 @@ export default function App() {
106131 URL . revokeObjectURL ( url ) ;
107132 } ;
108133
109- if ( loading ) return < div className = "p-6" > Loading feeds...</ div > ;
134+ const handleAddNew = ( ) => {
135+ // Placeholder for Google Form integration
136+ window . open ( 'https://forms.google.com' , '_blank' ) ;
137+ } ;
138+
139+ const handleSearch = ( query : string ) => {
140+ setSearchQuery ( query ) ;
141+ } ;
142+
143+ if ( loading ) return (
144+ < div className = "min-h-screen bg-[var(--background)] text-[var(--foreground)] flex flex-col" >
145+ < Navbar title = "RSS CSE Setup" onAddNew = { handleAddNew } onSearch = { handleSearch } />
146+ < div className = "flex-1 flex items-center justify-center" >
147+ < div className = "text-center" >
148+ < div className = "text-lg mb-2" > Loading feeds...</ div >
149+ < div className = "animate-spin text-2xl" > ⏳</ div >
150+ </ div >
151+ </ div >
152+ < Footer />
153+ </ div >
154+ ) ;
110155
111156 return (
112- < div className = "min-h-screen bg-slate-900 text-slate-100 p-6" >
113- < div className = "max-w-5xl mx-auto" >
114- < header className = "flex items-center justify-between mb-6" >
115- < h1 className = "text-3xl font-bold" > rss-cse-picker</ h1 >
116- < a href = "#" className = "text-sm underline" > Edit feeds.xlsx → convert → commit</ a >
117- </ header >
157+ < div className = "min-h-screen bg-[var(--background)] text-[var(--foreground)] flex flex-col transition-colors duration-300" >
158+ < Navbar title = "RSS CSE Setup" onAddNew = { handleAddNew } onSearch = { handleSearch } />
159+
160+ < main className = "flex-1 w-full px-4 py-8" >
161+ < div className = "mb-6 text-center" >
162+ < p className = "text-lg text-gray-400 mb-4" >
163+ Select RSS feeds from various Computer Science and Engineering categories to generate your OPML file.
164+ </ p >
165+ { searchQuery && (
166+ < p className = "text-sm text-gray-500 mt-2" >
167+ Showing results for: "{ searchQuery } "
168+ </ p >
169+ ) }
170+ </ div >
118171
119172 < section >
120- { data . map ( ( cat ) => (
121- < div key = { cat . category } className = "mb-6 bg-slate-800 p-4 rounded-lg" >
173+ { filteredData . length === 0 && searchQuery ? (
174+ < div className = "text-center py-12" >
175+ < p className = "text-gray-400 text-lg" > No feeds found matching "{ searchQuery } "</ p >
176+ < p className = "text-gray-500 text-sm mt-2" > Try adjusting your search terms</ p >
177+ </ div >
178+ ) : (
179+ filteredData . map ( ( cat ) => (
180+ < div key = { cat . category } className = "mb-6 bg-[var(--card-bg)] p-4 rounded-lg shadow-lg border border-gray-700" >
122181 < div className = "flex items-center justify-between mb-3" >
123- < h2 className = "text-xl font-semibold" > { cat . category } </ h2 >
182+ < h2 className = "text-xl font-semibold text-[var(--foreground)] " > { cat . category } </ h2 >
124183 < div className = "space-x-2" >
125- < button onClick = { ( ) => selectAllCategory ( cat . category , true ) } className = "px-3 py-1 rounded bg-green-600 text-sm" > Select all</ button >
126- < button onClick = { ( ) => selectAllCategory ( cat . category , false ) } className = "px-3 py-1 rounded bg-red-600 text-sm" > Clear</ button >
184+ < button
185+ onClick = { ( ) => selectAllCategory ( cat . category , true ) }
186+ className = "px-3 py-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm font-medium transition-colors"
187+ >
188+ Select all
189+ </ button >
190+ < button
191+ onClick = { ( ) => selectAllCategory ( cat . category , false ) }
192+ className = "px-3 py-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm font-medium transition-colors"
193+ >
194+ Clear
195+ </ button >
127196 </ div >
128197 </ div >
129198
130- < div className = "grid grid-cols-1 sm:grid-cols-2 gap-2 " >
199+ < div className = "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 " >
131200 { cat . feeds . map ( f => {
132201 const k = feedKey ( cat . category , f ) ;
133202 return (
134- < label key = { k } className = { `flex items-start gap-3 p-2 rounded ${ selected [ k ] ? 'bg-slate-700' : 'bg-slate-800' } ` } >
135- < input type = "checkbox" checked = { ! ! selected [ k ] } onChange = { ( ) => toggleFeed ( cat . category , f ) } />
136- < div >
137- < div className = "font-medium" > { f . title || f . xmlUrl } </ div >
138- < div className = "text-xs text-slate-400" > { f . htmlUrl || f . xmlUrl } </ div >
203+ < label
204+ key = { k }
205+ className = { `flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-all duration-200 ${
206+ selected [ k ]
207+ ? 'bg-indigo-600/20 border border-indigo-500'
208+ : 'bg-[var(--button-bg)] hover:bg-gray-600 border border-transparent'
209+ } `}
210+ >
211+ < input
212+ type = "checkbox"
213+ checked = { ! ! selected [ k ] }
214+ onChange = { ( ) => toggleFeed ( cat . category , f ) }
215+ className = "mt-1 w-4 h-4 accent-indigo-500"
216+ />
217+ < div className = "flex-1 min-w-0" >
218+ < div className = "font-medium text-[var(--foreground)] truncate" > { f . title || f . xmlUrl } </ div >
219+ < div className = "text-xs text-gray-400 truncate" > { f . htmlUrl || f . xmlUrl } </ div >
139220 </ div >
140221 </ label >
141222 ) ;
142223 } ) }
143224 </ div >
144225 </ div >
145- ) ) }
226+ ) )
227+ ) }
146228 </ section >
147229
148- < div className = "mt-6 flex gap-3" >
149- < button onClick = { generateOpml } className = "px-4 py-2 bg-indigo-600 rounded font-semibold" > Generate OPML</ button >
150- < button onClick = { copyToClipboard } className = "px-4 py-2 bg-blue-600 rounded" > Copy OPML</ button >
151- < button onClick = { downloadOpml } className = "px-4 py-2 bg-emerald-600 rounded" > Download OPML</ button >
230+ < div className = "mt-8 flex flex-wrap gap-3" >
231+ < button
232+ onClick = { generateOpml }
233+ className = "px-6 py-3 bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white font-semibold rounded-full shadow-lg transition duration-300"
234+ >
235+ Generate OPML
236+ </ button >
237+ < button
238+ onClick = { copyToClipboard }
239+ className = "px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-semibold rounded-full shadow-lg transition duration-300"
240+ >
241+ Copy OPML
242+ </ button >
243+ < button
244+ onClick = { downloadOpml }
245+ className = "px-6 py-3 bg-gradient-to-r from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700 text-white font-semibold rounded-full shadow-lg transition duration-300"
246+ >
247+ Download OPML
248+ </ button >
152249 </ div >
153250
154- < div className = "mt-6" >
155- < textarea value = { opml } readOnly rows = { 14 } className = "w-full bg-slate-900 border border-slate-700 p-3 rounded font-mono" > </ textarea >
251+ < div className = "mt-8" >
252+ < h3 className = "text-lg font-semibold mb-3 text-[var(--foreground)]" > Generated OPML:</ h3 >
253+ < textarea
254+ value = { opml }
255+ readOnly
256+ rows = { 8 }
257+ className = "w-full bg-[var(--card-bg)] text-[var(--foreground)] border border-gray-600 rounded-lg p-4 font-mono resize-none transition-colors duration-300 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
258+ placeholder = "Generated OPML will appear here..."
259+ />
156260 </ div >
157- </ div >
261+ </ main >
262+
263+ < Footer
264+ githubUrl = "https://github.com/your-username/rss-cse-setup"
265+ linkedinUrl = "https://linkedin.com/in/your-profile"
266+ codeCompassUrl = "https://codecompass.com/your-profile"
267+ />
158268 </ div >
159269 ) ;
160270}
0 commit comments