@@ -4,6 +4,7 @@ let categories = new Set();
44let bookmarks = new Set ( ) ;
55let likes = new Map ( ) ;
66let learningPaths = [ ] ;
7+ let searchDebounceTimer = null ;
78
89// Load from localStorage
910function loadFromStorage ( ) {
@@ -32,21 +33,64 @@ function saveToStorage() {
3233
3334// Fetch and parse repository data
3435async function fetchRepoData ( ) {
36+ const loadingText = document . getElementById ( 'loading-text' ) ;
37+ const loadingStats = document . getElementById ( 'loading-stats' ) ;
38+ const progressBar = document . getElementById ( 'loading-progress-bar' ) ;
39+
3540 try {
41+ loadingText . textContent = 'Fetching repository data...' ;
42+
3643 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 {
44+ if ( ! response . ok ) {
4445 throw new Error ( 'JSON not found' ) ;
4546 }
47+
48+ const totalSize = response . headers . get ( 'content-length' ) ;
49+ const reader = response . body . getReader ( ) ;
50+ let receivedLength = 0 ;
51+ let chunks = [ ] ;
52+
53+ while ( true ) {
54+ const { done, value} = await reader . read ( ) ;
55+
56+ if ( done ) break ;
57+
58+ chunks . push ( value ) ;
59+ receivedLength += value . length ;
60+
61+ if ( totalSize ) {
62+ const progress = ( receivedLength / totalSize ) * 100 ;
63+ progressBar . style . width = `${ progress } %` ;
64+ loadingStats . textContent = `Downloaded ${ ( receivedLength / 1024 / 1024 ) . toFixed ( 2 ) } MB of ${ ( totalSize / 1024 / 1024 ) . toFixed ( 2 ) } MB` ;
65+ }
66+ }
67+
68+ loadingText . textContent = 'Processing repository data...' ;
69+ progressBar . style . width = '100%' ;
70+
71+ const chunksAll = new Uint8Array ( receivedLength ) ;
72+ let position = 0 ;
73+ for ( let chunk of chunks ) {
74+ chunksAll . set ( chunk , position ) ;
75+ position += chunk . length ;
76+ }
77+
78+ const result = new TextDecoder ( "utf-8" ) . decode ( chunksAll ) ;
79+ const data = JSON . parse ( result ) ;
80+
81+ allRepos = Array . isArray ( data ) ? data : ( data . repos || [ ] ) ;
82+ allRepos . forEach ( repo => categories . add ( repo . category ) ) ;
83+
84+ loadingStats . textContent = `Loaded ${ allRepos . length . toLocaleString ( ) } repositories across ${ categories . size } categories` ;
85+ console . log ( `Loaded ${ allRepos . length } repositories from JSON` ) ;
86+
87+ showToast ( 'success' , 'Data Loaded' , `${ allRepos . length . toLocaleString ( ) } repositories ready to explore` ) ;
4688 } catch ( e ) {
4789 console . error ( 'Error loading repo data:' , e ) ;
48- // Show error message
90+ loadingText . textContent = 'Error loading repositories' ;
91+ loadingStats . textContent = 'Please refresh the page to try again' ;
4992 document . getElementById ( 'results-count' ) . textContent = 'Error loading repositories' ;
93+ showToast ( 'error' , 'Loading Failed' , 'Could not load repository data. Please refresh the page.' ) ;
5094 }
5195}
5296
@@ -188,10 +232,20 @@ function renderRepos(repos) {
188232
189233// Toggle bookmark
190234function toggleBookmark ( repoId ) {
191- if ( bookmarks . has ( repoId ) ) {
235+ const wasBookmarked = bookmarks . has ( repoId ) ;
236+
237+ if ( wasBookmarked ) {
192238 bookmarks . delete ( repoId ) ;
239+ showToast ( 'info' , 'Bookmark Removed' , 'Repository removed from bookmarks' ) ;
193240 } else {
194241 bookmarks . add ( repoId ) ;
242+ showToast ( 'success' , 'Bookmark Added' , 'Repository saved to bookmarks' ) ;
243+ // Add animation to the button
244+ const btn = document . querySelector ( `.bookmark-btn[data-repo-id="${ repoId } "]` ) ;
245+ if ( btn ) {
246+ btn . classList . add ( 'animating' ) ;
247+ setTimeout ( ( ) => btn . classList . remove ( 'animating' ) , 400 ) ;
248+ }
195249 }
196250 saveToStorage ( ) ;
197251 updateBookmarkCount ( ) ;
@@ -209,18 +263,53 @@ function toggleLike(repoId) {
209263 const currentLikes = likes . get ( repoId ) || 0 ;
210264 likes . set ( repoId , currentLikes + 1 ) ;
211265 saveToStorage ( ) ;
266+
267+ // Add animation to the button
268+ const btn = document . querySelector ( `.like-btn[data-repo-id="${ repoId } "]` ) ;
269+ if ( btn ) {
270+ btn . classList . add ( 'animating' ) ;
271+ setTimeout ( ( ) => btn . classList . remove ( 'animating' ) , 400 ) ;
272+ }
273+
212274 renderRepos ( filterRepos ( ) ) ;
213275 updateStats ( ) ;
276+ showToast ( 'success' , 'Liked!' , 'Thank you for your feedback' ) ;
214277}
215278
216279// Share repo
217280function shareRepo ( repoId ) {
218281 const repo = allRepos . find ( r => r . id === repoId ) ;
219282 if ( ! repo ) return ;
220283
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' ) ;
284+ // Try to use modern share API first
285+ if ( navigator . share ) {
286+ navigator . share ( {
287+ title : repo . name ,
288+ text : repo . description ,
289+ url : repo . url
290+ } ) . then ( ( ) => {
291+ showToast ( 'success' , 'Shared!' , 'Repository link shared successfully' ) ;
292+ } ) . catch ( ( error ) => {
293+ if ( error . name !== 'AbortError' ) {
294+ fallbackShare ( repo ) ;
295+ }
296+ } ) ;
297+ } else {
298+ fallbackShare ( repo ) ;
299+ }
300+ }
301+
302+ function fallbackShare ( repo ) {
303+ // Copy to clipboard as fallback
304+ const text = `${ repo . name } : ${ repo . description } \n${ repo . url } ` ;
305+ navigator . clipboard . writeText ( text ) . then ( ( ) => {
306+ showToast ( 'success' , 'Link Copied!' , 'Repository link copied to clipboard' ) ;
307+ } ) . catch ( ( ) => {
308+ // Fallback to Twitter share
309+ const tweetText = `Check out ${ repo . name } - ${ repo . description } #AwesomeStargazer #GitHub` ;
310+ const url = `https://twitter.com/intent/tweet?text=${ encodeURIComponent ( tweetText ) } &url=${ encodeURIComponent ( repo . url ) } ` ;
311+ window . open ( url , '_blank' , 'width=550,height=420' ) ;
312+ } ) ;
224313}
225314
226315// Add to learning path
@@ -229,8 +318,8 @@ function addToPath(repoId) {
229318 if ( ! repo ) return ;
230319
231320 if ( learningPaths . length === 0 ) {
232- alert ( 'Please create a learning path first! ') ;
233- switchTab ( 'learning-paths' ) ;
321+ showToast ( 'info' , 'No Learning Paths' , 'Create a learning path first to add repositories ') ;
322+ setTimeout ( ( ) => switchTab ( 'learning-paths' ) , 1500 ) ;
234323 return ;
235324 }
236325
@@ -249,9 +338,9 @@ function addToPath(repoId) {
249338 completed : false
250339 } ) ;
251340 saveToStorage ( ) ;
252- alert ( `Added to "${ learningPaths [ index ] . name } "! ` ) ;
341+ showToast ( 'success' , 'Added to Path!' , `Added to "${ learningPaths [ index ] . name } "` ) ;
253342 } else {
254- alert ( ' Repository already in this path! ') ;
343+ showToast ( 'info' , 'Already Added' , ' Repository is already in this learning path') ;
255344 }
256345 }
257346 }
@@ -516,7 +605,11 @@ async function init() {
516605
517606 // Event listeners
518607 document . getElementById ( 'search-input' ) . addEventListener ( 'input' , ( ) => {
519- renderRepos ( filterRepos ( ) ) ;
608+ // Debounce search for better performance
609+ clearTimeout ( searchDebounceTimer ) ;
610+ searchDebounceTimer = setTimeout ( ( ) => {
611+ renderRepos ( filterRepos ( ) ) ;
612+ } , 300 ) ;
520613 } ) ;
521614
522615 document . getElementById ( 'search-clear' ) . addEventListener ( 'click' , ( ) => {
@@ -532,6 +625,15 @@ async function init() {
532625 renderRepos ( filterRepos ( ) ) ;
533626 } ) ;
534627
628+ // Reset filters button
629+ document . getElementById ( 'reset-filters-btn' ) . addEventListener ( 'click' , ( ) => {
630+ document . getElementById ( 'search-input' ) . value = '' ;
631+ document . getElementById ( 'category-filter' ) . value = '' ;
632+ document . getElementById ( 'sort-filter' ) . value = 'name-asc' ;
633+ renderRepos ( filterRepos ( ) ) ;
634+ showToast ( 'info' , 'Filters Reset' , 'All filters have been cleared' ) ;
635+ } ) ;
636+
535637 // Tab navigation
536638 document . querySelectorAll ( '.nav-btn' ) . forEach ( btn => {
537639 btn . addEventListener ( 'click' , ( ) => {
@@ -642,5 +744,36 @@ function showHelpModal() {
642744 document . getElementById ( 'help-modal' ) . classList . remove ( 'hidden' ) ;
643745}
644746
747+ // Toast notification system
748+ function showToast ( type , title , message ) {
749+ const container = document . getElementById ( 'toast-container' ) ;
750+ const toast = document . createElement ( 'div' ) ;
751+ toast . className = `toast ${ type } ` ;
752+
753+ const icons = {
754+ 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>' ,
755+ 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>' ,
756+ 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>'
757+ } ;
758+
759+ toast . innerHTML = `
760+ <div class="toast-icon">${ icons [ type ] } </div>
761+ <div class="toast-content">
762+ <div class="toast-title">${ title } </div>
763+ <div class="toast-message">${ message } </div>
764+ </div>
765+ ` ;
766+
767+ container . appendChild ( toast ) ;
768+
769+ // Auto remove after 4 seconds
770+ setTimeout ( ( ) => {
771+ toast . classList . add ( 'removing' ) ;
772+ setTimeout ( ( ) => {
773+ container . removeChild ( toast ) ;
774+ } , 300 ) ;
775+ } , 4000 ) ;
776+ }
777+
645778// Start the app
646779init ( ) ;
0 commit comments