Skip to content

Commit 4682417

Browse files
Copilot0xrinegade
andcommitted
Add enhanced UX/UI features: loading progress, toast notifications, animations
Co-authored-by: 0xrinegade <[email protected]>
1 parent 5c924e3 commit 4682417

File tree

3 files changed

+403
-18
lines changed

3 files changed

+403
-18
lines changed

app.js

Lines changed: 150 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ let categories = new Set();
44
let bookmarks = new Set();
55
let likes = new Map();
66
let learningPaths = [];
7+
let searchDebounceTimer = null;
78

89
// Load from localStorage
910
function loadFromStorage() {
@@ -32,21 +33,64 @@ function saveToStorage() {
3233

3334
// Fetch and parse repository data
3435
async 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
190234
function 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
217280
function 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
646779
init();

index.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,25 @@ <h1 class="site-title">Awesome Stargazer</h1>
110110
<div id="repos-container" class="repos-grid"></div>
111111
<div id="loading-state" class="loading-state">
112112
<div class="loading-spinner"></div>
113-
<p>Loading repositories...</p>
113+
<p id="loading-text">Loading repositories...</p>
114+
<div class="loading-progress">
115+
<div class="loading-progress-bar" id="loading-progress-bar"></div>
116+
</div>
117+
<p class="loading-stats" id="loading-stats"></p>
114118
</div>
115119
<div id="no-results" class="no-results hidden">
116120
<svg width="64" height="64" fill="currentColor" viewBox="0 0 16 16">
117121
<path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04zM11.5 7a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z"></path>
118122
</svg>
119123
<h3>No repositories found</h3>
120124
<p>Try adjusting your search or filters</p>
125+
<button class="btn btn-secondary" id="reset-filters-btn" style="margin-top: 16px;">
126+
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
127+
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"></path>
128+
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"></path>
129+
</svg>
130+
Reset Filters
131+
</button>
121132
</div>
122133
</div>
123134
</div>
@@ -270,6 +281,9 @@ <h2>⌨️ Keyboard Shortcuts</h2>
270281
</div>
271282
</div>
272283

284+
<!-- Toast notifications -->
285+
<div id="toast-container" class="toast-container"></div>
286+
273287
<script src="app.js"></script>
274288
</body>
275289
</html>

0 commit comments

Comments
 (0)