@@ -4,6 +4,45 @@ let categories = new Set();
44let bookmarks = new Set ( ) ;
55let likes = new Map ( ) ;
66let learningPaths = [ ] ;
7+ let searchDebounceTimer = null ;
8+
9+ // Category icon mapping
10+ function getCategoryIcon ( category ) {
11+ const iconMap = {
12+ 'AI' : '🤖' ,
13+ 'BLOCKCHAIN' : '⛓️' ,
14+ 'SOLANA' : '◎' ,
15+ 'ETHEREUM' : '⟠' ,
16+ 'REACT' : '⚛️' ,
17+ 'RUST' : '🦀' ,
18+ 'PYTHON' : '🐍' ,
19+ 'JAVASCRIPT' : '📜' ,
20+ 'TYPESCRIPT' : '💙' ,
21+ 'GO' : '🐹' ,
22+ 'DATABASE' : '🗄️' ,
23+ 'WEB' : '🌐' ,
24+ 'MOBILE' : '📱' ,
25+ 'SECURITY' : '🔒' ,
26+ 'DEVOPS' : '🚀' ,
27+ 'TESTING' : '✅' ,
28+ 'GAMING' : '🎮' ,
29+ 'MACHINE LEARNING' : '🧠' ,
30+ 'NLP' : '💬' ,
31+ 'COMPUTER VISION' : '👁️' ,
32+ 'DATA' : '📊' ,
33+ 'CLOUD' : '☁️'
34+ } ;
35+
36+ // Try to match category with icon map
37+ const upperCategory = category . toUpperCase ( ) ;
38+ for ( const [ key , icon ] of Object . entries ( iconMap ) ) {
39+ if ( upperCategory . includes ( key ) ) {
40+ return icon ;
41+ }
42+ }
43+
44+ return '📦' ; // Default icon
45+ }
746
847// Load from localStorage
948function loadFromStorage ( ) {
@@ -32,21 +71,64 @@ function saveToStorage() {
3271
3372// Fetch and parse repository data
3473async function fetchRepoData ( ) {
74+ const loadingText = document . getElementById ( 'loading-text' ) ;
75+ const loadingStats = document . getElementById ( 'loading-stats' ) ;
76+ const progressBar = document . getElementById ( 'loading-progress-bar' ) ;
77+
3578 try {
79+ loadingText . textContent = 'Fetching repository data...' ;
80+
3681 const response = await fetch ( 'data.json' ) ;
37- if ( response . ok ) {
38- const data = await response . json ( ) ;
39- // Check if data has repos property or is array
40- allRepos = Array . isArray ( data ) ? data : ( data . repos || [ ] ) ;
41- allRepos . forEach ( repo => categories . add ( repo . category ) ) ;
42- console . log ( `Loaded ${ allRepos . length } repositories from JSON` ) ;
43- } else {
82+ if ( ! response . ok ) {
4483 throw new Error ( 'JSON not found' ) ;
4584 }
85+
86+ const totalSize = response . headers . get ( 'content-length' ) ;
87+ const reader = response . body . getReader ( ) ;
88+ let receivedLength = 0 ;
89+ let chunks = [ ] ;
90+
91+ while ( true ) {
92+ const { done, value} = await reader . read ( ) ;
93+
94+ if ( done ) break ;
95+
96+ chunks . push ( value ) ;
97+ receivedLength += value . length ;
98+
99+ if ( totalSize ) {
100+ const progress = ( receivedLength / totalSize ) * 100 ;
101+ progressBar . style . width = `${ progress } %` ;
102+ loadingStats . textContent = `Downloaded ${ ( receivedLength / 1024 / 1024 ) . toFixed ( 2 ) } MB of ${ ( totalSize / 1024 / 1024 ) . toFixed ( 2 ) } MB` ;
103+ }
104+ }
105+
106+ loadingText . textContent = 'Processing repository data...' ;
107+ progressBar . style . width = '100%' ;
108+
109+ const chunksAll = new Uint8Array ( receivedLength ) ;
110+ let position = 0 ;
111+ for ( let chunk of chunks ) {
112+ chunksAll . set ( chunk , position ) ;
113+ position += chunk . length ;
114+ }
115+
116+ const result = new TextDecoder ( "utf-8" ) . decode ( chunksAll ) ;
117+ const data = JSON . parse ( result ) ;
118+
119+ allRepos = Array . isArray ( data ) ? data : ( data . repos || [ ] ) ;
120+ allRepos . forEach ( repo => categories . add ( repo . category ) ) ;
121+
122+ loadingStats . textContent = `Loaded ${ allRepos . length . toLocaleString ( ) } repositories across ${ categories . size } categories` ;
123+ console . log ( `Loaded ${ allRepos . length } repositories from JSON` ) ;
124+
125+ showToast ( 'success' , 'Data Loaded' , `${ allRepos . length . toLocaleString ( ) } repositories ready to explore` ) ;
46126 } catch ( e ) {
47127 console . error ( 'Error loading repo data:' , e ) ;
48- // Show error message
128+ loadingText . textContent = 'Error loading repositories' ;
129+ loadingStats . textContent = 'Please refresh the page to try again' ;
49130 document . getElementById ( 'results-count' ) . textContent = 'Error loading repositories' ;
131+ showToast ( 'error' , 'Loading Failed' , 'Could not load repository data. Please refresh the page.' ) ;
50132 }
51133}
52134
@@ -115,11 +197,12 @@ function renderRepos(repos) {
115197 const isBookmarked = bookmarks . has ( repo . id ) ;
116198 const likeCount = likes . get ( repo . id ) || 0 ;
117199 const isLiked = likeCount > 0 ;
200+ const categoryIcon = getCategoryIcon ( repo . category ) ;
118201
119202 return `
120203 <div class="repo-card" data-repo-id="${ repo . id } ">
121204 <div class="repo-header">
122- <div class="repo-icon">📦 </div>
205+ <div class="repo-icon">${ categoryIcon } </div>
123206 <div class="repo-info">
124207 <a href="${ repo . url } " class="repo-name" target="_blank" rel="noopener">${ repo . name } </a>
125208 <div class="repo-category">${ repo . category } </div>
@@ -188,10 +271,20 @@ function renderRepos(repos) {
188271
189272// Toggle bookmark
190273function toggleBookmark ( repoId ) {
191- if ( bookmarks . has ( repoId ) ) {
274+ const wasBookmarked = bookmarks . has ( repoId ) ;
275+
276+ if ( wasBookmarked ) {
192277 bookmarks . delete ( repoId ) ;
278+ showToast ( 'info' , 'Bookmark Removed' , 'Repository removed from bookmarks' ) ;
193279 } else {
194280 bookmarks . add ( repoId ) ;
281+ showToast ( 'success' , 'Bookmark Added' , 'Repository saved to bookmarks' ) ;
282+ // Add animation to the button
283+ const btn = document . querySelector ( `.bookmark-btn[data-repo-id="${ repoId } "]` ) ;
284+ if ( btn ) {
285+ btn . classList . add ( 'animating' ) ;
286+ setTimeout ( ( ) => btn . classList . remove ( 'animating' ) , 400 ) ;
287+ }
195288 }
196289 saveToStorage ( ) ;
197290 updateBookmarkCount ( ) ;
@@ -209,18 +302,53 @@ function toggleLike(repoId) {
209302 const currentLikes = likes . get ( repoId ) || 0 ;
210303 likes . set ( repoId , currentLikes + 1 ) ;
211304 saveToStorage ( ) ;
305+
306+ // Add animation to the button
307+ const btn = document . querySelector ( `.like-btn[data-repo-id="${ repoId } "]` ) ;
308+ if ( btn ) {
309+ btn . classList . add ( 'animating' ) ;
310+ setTimeout ( ( ) => btn . classList . remove ( 'animating' ) , 400 ) ;
311+ }
312+
212313 renderRepos ( filterRepos ( ) ) ;
213314 updateStats ( ) ;
315+ showToast ( 'success' , 'Liked!' , 'Thank you for your feedback' ) ;
214316}
215317
216318// Share repo
217319function shareRepo ( repoId ) {
218320 const repo = allRepos . find ( r => r . id === repoId ) ;
219321 if ( ! repo ) return ;
220322
221- const text = `Check out ${ repo . name } - ${ repo . description } #AwesomeStargazer #GitHub` ;
222- const url = `https://twitter.com/intent/tweet?text=${ encodeURIComponent ( text ) } &url=${ encodeURIComponent ( repo . url ) } ` ;
223- window . open ( url , '_blank' , 'width=550,height=420' ) ;
323+ // Try to use modern share API first
324+ if ( navigator . share ) {
325+ navigator . share ( {
326+ title : repo . name ,
327+ text : repo . description ,
328+ url : repo . url
329+ } ) . then ( ( ) => {
330+ showToast ( 'success' , 'Shared!' , 'Repository link shared successfully' ) ;
331+ } ) . catch ( ( error ) => {
332+ if ( error . name !== 'AbortError' ) {
333+ fallbackShare ( repo ) ;
334+ }
335+ } ) ;
336+ } else {
337+ fallbackShare ( repo ) ;
338+ }
339+ }
340+
341+ function fallbackShare ( repo ) {
342+ // Copy to clipboard as fallback
343+ const text = `${ repo . name } : ${ repo . description } \n${ repo . url } ` ;
344+ navigator . clipboard . writeText ( text ) . then ( ( ) => {
345+ showToast ( 'success' , 'Link Copied!' , 'Repository link copied to clipboard' ) ;
346+ } ) . catch ( ( ) => {
347+ // Fallback to Twitter share
348+ const tweetText = `Check out ${ repo . name } - ${ repo . description } #AwesomeStargazer #GitHub` ;
349+ const url = `https://twitter.com/intent/tweet?text=${ encodeURIComponent ( tweetText ) } &url=${ encodeURIComponent ( repo . url ) } ` ;
350+ window . open ( url , '_blank' , 'width=550,height=420' ) ;
351+ } ) ;
224352}
225353
226354// Add to learning path
@@ -229,8 +357,8 @@ function addToPath(repoId) {
229357 if ( ! repo ) return ;
230358
231359 if ( learningPaths . length === 0 ) {
232- alert ( 'Please create a learning path first! ') ;
233- switchTab ( 'learning-paths' ) ;
360+ showToast ( 'info' , 'No Learning Paths' , 'Create a learning path first to add repositories ') ;
361+ setTimeout ( ( ) => switchTab ( 'learning-paths' ) , 1500 ) ;
234362 return ;
235363 }
236364
@@ -249,9 +377,9 @@ function addToPath(repoId) {
249377 completed : false
250378 } ) ;
251379 saveToStorage ( ) ;
252- alert ( `Added to "${ learningPaths [ index ] . name } "! ` ) ;
380+ showToast ( 'success' , 'Added to Path!' , `Added to "${ learningPaths [ index ] . name } "` ) ;
253381 } else {
254- alert ( ' Repository already in this path! ') ;
382+ showToast ( 'info' , 'Already Added' , ' Repository is already in this learning path') ;
255383 }
256384 }
257385 }
@@ -516,7 +644,11 @@ async function init() {
516644
517645 // Event listeners
518646 document . getElementById ( 'search-input' ) . addEventListener ( 'input' , ( ) => {
519- renderRepos ( filterRepos ( ) ) ;
647+ // Debounce search for better performance
648+ clearTimeout ( searchDebounceTimer ) ;
649+ searchDebounceTimer = setTimeout ( ( ) => {
650+ renderRepos ( filterRepos ( ) ) ;
651+ } , 300 ) ;
520652 } ) ;
521653
522654 document . getElementById ( 'search-clear' ) . addEventListener ( 'click' , ( ) => {
@@ -532,6 +664,15 @@ async function init() {
532664 renderRepos ( filterRepos ( ) ) ;
533665 } ) ;
534666
667+ // Reset filters button
668+ document . getElementById ( 'reset-filters-btn' ) . addEventListener ( 'click' , ( ) => {
669+ document . getElementById ( 'search-input' ) . value = '' ;
670+ document . getElementById ( 'category-filter' ) . value = '' ;
671+ document . getElementById ( 'sort-filter' ) . value = 'name-asc' ;
672+ renderRepos ( filterRepos ( ) ) ;
673+ showToast ( 'info' , 'Filters Reset' , 'All filters have been cleared' ) ;
674+ } ) ;
675+
535676 // Tab navigation
536677 document . querySelectorAll ( '.nav-btn' ) . forEach ( btn => {
537678 btn . addEventListener ( 'click' , ( ) => {
@@ -642,5 +783,36 @@ function showHelpModal() {
642783 document . getElementById ( 'help-modal' ) . classList . remove ( 'hidden' ) ;
643784}
644785
786+ // Toast notification system
787+ function showToast ( type , title , message ) {
788+ const container = document . getElementById ( 'toast-container' ) ;
789+ const toast = document . createElement ( 'div' ) ;
790+ toast . className = `toast ${ type } ` ;
791+
792+ const icons = {
793+ success : '<svg width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm3.78-9.72a.75.75 0 0 0-1.06-1.06L6.75 9.19 5.28 7.72a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l4.5-4.5z"></path></svg>' ,
794+ error : '<svg width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"></path></svg>' ,
795+ info : '<svg width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"></path></svg>'
796+ } ;
797+
798+ toast . innerHTML = `
799+ <div class="toast-icon">${ icons [ type ] } </div>
800+ <div class="toast-content">
801+ <div class="toast-title">${ title } </div>
802+ <div class="toast-message">${ message } </div>
803+ </div>
804+ ` ;
805+
806+ container . appendChild ( toast ) ;
807+
808+ // Auto remove after 4 seconds
809+ setTimeout ( ( ) => {
810+ toast . classList . add ( 'removing' ) ;
811+ setTimeout ( ( ) => {
812+ container . removeChild ( toast ) ;
813+ } , 300 ) ;
814+ } , 4000 ) ;
815+ }
816+
645817// Start the app
646818init ( ) ;
0 commit comments