Skip to content

Commit e08bd1a

Browse files
authored
Merge pull request #4 from openSVM/copilot/self-ask-and-refine-ui
Enhance UX/UI with loading progress, toast notifications, accessibility improvements, and visual polish
2 parents 8ae9b58 + 537f8b7 commit e08bd1a

File tree

5 files changed

+838
-23
lines changed

5 files changed

+838
-23
lines changed

app.js

Lines changed: 190 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,45 @@ let categories = new Set();
44
let bookmarks = new Set();
55
let likes = new Map();
66
let 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
948
function loadFromStorage() {
@@ -32,21 +71,64 @@ function saveToStorage() {
3271

3372
// Fetch and parse repository data
3473
async 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
190273
function 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
217319
function 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
646818
init();

data.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
repo-data.json

0 commit comments

Comments
 (0)